diff --git a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 index c3f9ddc7b..8d4e5ce60 100644 --- a/adapters/powershell/Tests/powershellgroup.config.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.config.tests.ps1 @@ -293,6 +293,9 @@ Describe 'PowerShell adapter resource tests' { if ($metadata -eq 'Microsoft.DSC') { "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) } + if ($adapter -eq 'Microsoft.DSC/PowerShell') { + (Get-Content -Raw -Path $TestDrive/tracing.txt) | Should -Match "Resource 'Microsoft.DSC/PowerShell' is deprecated" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) + } } It 'Config works with credential object' { diff --git a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 index c5170ad7f..1825ae340 100644 --- a/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 +++ b/adapters/powershell/Tests/powershellgroup.resource.tests.ps1 @@ -368,7 +368,7 @@ Describe 'PowerShell adapter resource tests' { It 'Specifying a non-existent version returns an error' { $null = dsc resource get -r TestClassResource/TestClassResource --version 0.0.2 2> $TestDrive/error.log $LASTEXITCODE | Should -Be 7 - Get-Content -Path $TestDrive/error.log | Should -Match 'Resource not found: TestClassResource/TestClassResource 0.0.2' + (Get-Content -Raw -Path $TestDrive/error.log) | Should -BeLike '*Resource not found: TestClassResource/TestClassResource 0.0.2*' -Because (Get-Content -Raw -Path $TestDrive/error.log) } It 'Can process SecureString property' { diff --git a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 index f6b42fb37..5ad1ed0b3 100644 --- a/adapters/powershell/Tests/win_powershellgroup.tests.ps1 +++ b/adapters/powershell/Tests/win_powershellgroup.tests.ps1 @@ -279,7 +279,9 @@ resources: } if ($metadata -eq 'Microsoft.DSC') { "$TestDrive/tracing.txt" | Should -FileContentMatch "Invoking $Operation for '$adapter'" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) - + } + if ($adapter -eq 'Microsoft.Windows/WindowsPowerShell') { + (Get-Content -Raw -Path $TestDrive/tracing.txt) | Should -Match "Resource 'Microsoft.Windows/WindowsPowerShell' is deprecated" -Because (Get-Content -Raw -Path $TestDrive/tracing.txt) } } } diff --git a/adapters/powershell/powershell.dsc.resource.json b/adapters/powershell/powershell.dsc.resource.json index e481b0737..397649f4a 100644 --- a/adapters/powershell/powershell.dsc.resource.json +++ b/adapters/powershell/powershell.dsc.resource.json @@ -3,6 +3,7 @@ "type": "Microsoft.DSC/PowerShell", "version": "0.1.0", "kind": "adapter", + "deprecationMessage": "Use the 'Microsoft.Adapters/PowerShell' adapter instead.", "description": "Resource adapter to classic DSC Powershell resources.", "tags": [ "PowerShell" diff --git a/adapters/powershell/windowspowershell.dsc.resource.json b/adapters/powershell/windowspowershell.dsc.resource.json index 297b44d05..b378f0b0f 100644 --- a/adapters/powershell/windowspowershell.dsc.resource.json +++ b/adapters/powershell/windowspowershell.dsc.resource.json @@ -3,6 +3,7 @@ "type": "Microsoft.Windows/WindowsPowerShell", "version": "0.1.0", "kind": "adapter", + "deprecationMessage": "Use the 'Microsoft.Adapters/WindowsPowerShell' adapter instead.", "description": "Resource adapter to classic DSC Powershell resources in Windows PowerShell.", "tags": [ "PowerShell" diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 index e8cb8035b..63381dc3c 100644 --- a/dsc/tests/dsc_adapter.tests.ps1 +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -227,5 +227,18 @@ Describe 'Tests for adapter support' { } } } + + It 'Deprecated adapted resource shows message' { + try { + $dscHome = Split-Path (Get-Command dsc).Source -Parent + $env:DSC_RESOURCE_PATH = (Join-Path -Path $dscHome -ChildPath 'deprecated') + [System.IO.Path]::PathSeparator + $dscHome + $out = dsc resource get -r Adapted/Deprecated -i '{}' 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) + $out | Should -Not -BeNullOrEmpty + (Get-Content $TestDrive/error.log -Raw) | Should -Match "Resource 'Adapted/Deprecated' is deprecated: This adapted resource is deprecated" -Because (Get-Content $TestDrive/error.log | Out-String) + } finally { + $env:DSC_RESOURCE_PATH = $null + } + } } } diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index af86d5cf2..2be1256db 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -135,4 +135,17 @@ Describe 'Discover extension tests' { $env:PATH = $oldPath } } + + It 'Deprecated extension shows message' { + try { + $dscHome = Split-Path (Get-Command dsc).Source -Parent + $env:DSC_RESOURCE_PATH = (Join-Path -Path $dscHome -ChildPath 'deprecated') + [System.IO.Path]::PathSeparator + $dscHome + + $null = dsc resource list 2> $TestDrive/error.log + $LASTEXITCODE | Should -Be 0 + (Get-Content -Path "$TestDrive/error.log" -Raw) | Should -Match "Extension 'Test/ExtensionDeprecated' is deprecated: This extension is deprecated" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String) + } finally { + $env:DSC_RESOURCE_PATH = $null + } + } } diff --git a/dsc/tests/dsc_extension_import.tests.ps1 b/dsc/tests/dsc_extension_import.tests.ps1 new file mode 100644 index 000000000..b824ae291 --- /dev/null +++ b/dsc/tests/dsc_extension_import.tests.ps1 @@ -0,0 +1,18 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Import extension tests' { + It 'Deprecated extension shows message' { + try { + $dscHome = Split-Path (Get-Command dsc).Source -Parent + $env:DSC_RESOURCE_PATH = (Join-Path -Path $dscHome -ChildPath 'deprecated') + [System.IO.Path]::PathSeparator + $dscHome + + Set-Content -Path "$TestDrive/test.testimport" -Value 'Test content' + $null = dsc config get -f "$TestDrive/test.testimport" 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 2 -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String) + (Get-Content -Path "$TestDrive/error.log" -Raw) | Should -Match "Extension 'Test/ExtensionDeprecated' is deprecated: This extension is deprecated" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String) + } finally { + $env:DSC_RESOURCE_PATH = $null + } + } +} diff --git a/dsc/tests/dsc_extension_secret.tests.ps1 b/dsc/tests/dsc_extension_secret.tests.ps1 index 5d70610e4..9496aa62c 100644 --- a/dsc/tests/dsc_extension_secret.tests.ps1 +++ b/dsc/tests/dsc_extension_secret.tests.ps1 @@ -168,4 +168,27 @@ Describe 'Tests for the secret() function and extensions' { $out.results.Count | Should -Be 1 $out.results[0].result.actualState.Output | Should -BeExactly 'Hello' } + + It 'Deprecated extension shows message' { + try { + $dscHome = Split-Path (Get-Command dsc).Source -Parent + $env:DSC_RESOURCE_PATH = (Join-Path -Path $dscHome -ChildPath 'deprecated') + [System.IO.Path]::PathSeparator + $dscHome + + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + variables: + myString: "[secret('nonExisting')]" + resources: + - name: Database Connection + type: Microsoft.DSC.Debug/Echo + properties: + output: "[variables('myString')]" +'@ + dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 4 + (Get-Content -Raw -Path "$TestDrive/error.log") | Should -Match "Extension 'Test/ExtensionDeprecated' is deprecated: This extension is deprecated" -Because (Get-Content -Raw -Path "$TestDrive/error.log") + } finally { + $env:DSC_RESOURCE_PATH = $null + } + } } diff --git a/dsc/tests/dsc_resource_deprecated.tests.ps1 b/dsc/tests/dsc_resource_deprecated.tests.ps1 new file mode 100644 index 000000000..00f116c1e --- /dev/null +++ b/dsc/tests/dsc_resource_deprecated.tests.ps1 @@ -0,0 +1,56 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Deprecated resource tests' { + BeforeAll { + $dscHome = Split-Path (Get-Command dsc).Source -Parent + $env:DSC_RESOURCE_PATH = (Join-Path -Path $dscHome -ChildPath 'deprecated') + [System.IO.Path]::PathSeparator + $dscHome + } + + AfterAll { + $env:DSC_RESOURCE_PATH = $null + } + + It 'Deprecated resource for operation ' -TestCases @( + @{ operation = 'get' } + @{ operation = 'set' } + @{ operation = 'delete' } + @{ operation = 'test' } + @{ operation = 'export' } + ) { + param($operation) + + $out = dsc resource $operation -r Test/OperationDeprecated -i '{}' 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String) + if ($operation -eq 'delete') { + $out | Should -BeNullOrEmpty + } else { + $out | Should -Not -BeNullOrEmpty + } + (Get-Content $TestDrive/error.log -Raw) | Should -Match "Resource 'Test/OperationDeprecated' is deprecated: This resource is deprecated" + } + + It 'Deprecated resource for schema' { + $out = dsc resource schema -r Test/OperationDeprecated 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String) + $out | Should -Not -BeNullOrEmpty + (Get-Content $TestDrive/error.log -Raw) | Should -Match "Resource 'Test/OperationDeprecated' is deprecated: This resource is deprecated" + } + + It 'Deprecated message when used in config' { + $configYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: test + type: Test/OperationDeprecated + properties: + operation: get +'@ + + $out = dsc config get -i $configYaml 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String) + $out.results.count | Should -Be 1 + $out.results[0].type | Should -BeExactly 'Test/OperationDeprecated' + (Get-Content $TestDrive/error.log -Raw) | Should -Match "Resource 'Test/OperationDeprecated' is deprecated: This resource is deprecated" + } +} diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 03e64a2a0..ab36a40a9 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -220,6 +220,7 @@ adapterResourceNotFound = "Adapter resource '%{adapter}' not found" adapterManifestNotFound = "Adapter manifest for '%{adapter}' not found" adapterDoesNotSupportDelete = "Adapter '%{adapter}' does not support delete operation" validatingAgainstSchema = "Validating against resource schema" +deprecationMessage = "Resource '%{resource}' is deprecated: %{message}" [dscresources.resource_manifest] resourceManifestSchemaTitle = "Resource manifest schema URI" @@ -239,6 +240,7 @@ importNotSupported = "Import is not supported by extension '%{extension}' for fi importNoResults = "Extension '%{extension}' returned no results for import" secretMultipleLinesReturned = "Extension '%{extension}' returned multiple lines which is not supported for secrets" importProcessingOutput = "Processing output from extension '%{extension}'" +deprecationMessage = "Extension '%{extension}' is deprecated: %{message}" [extensions.extension_manifest] extensionManifestSchemaTitle = "Extension manifest schema URI" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 50eb15991..484a8dae9 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -795,6 +795,7 @@ fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceMani type_name: manifest.type_name.clone(), kind: Kind::Resource, implemented_as: None, + deprecation_message: manifest.deprecation_message.clone(), description: manifest.description.clone(), version: manifest.version.clone(), capabilities: manifest.capabilities.clone(), @@ -858,6 +859,7 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result Result< let extension = DscExtension { type_name: manifest.r#type.clone(), + deprecation_message: manifest.deprecation_message.clone(), description: manifest.description.clone(), version: manifest.version.clone(), capabilities, diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index a93357f4a..e9c3f7593 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -16,7 +16,7 @@ use serde_json::{Map, Value}; use std::path::PathBuf; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( base_name = "manifest", folder_path = "resource", @@ -43,6 +43,9 @@ pub struct AdaptedDscResourceManifest { pub capabilities: Vec, /// An optional condition for the resource to be active. pub condition: Option, + /// An optional message indicating the resource is deprecated. If provided, the message will be shown when the resource is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecation_message: Option, /// The file path to the resource. pub path: PathBuf, /// The description of the resource. @@ -50,7 +53,6 @@ pub struct AdaptedDscResourceManifest { /// The author of the resource. pub author: Option, /// The required resource adapter for the resource. - #[serde(rename="requireAdapter")] pub require_adapter: FullyQualifiedTypeName, /// The JSON Schema of the resource. pub schema: Map, diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index d772ad1ff..e37783321 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::collections::HashMap; use std::path::PathBuf; -use tracing::{debug, info, trace}; +use tracing::{debug, info, trace, warn}; use crate::schemas::dsc_repo::DscRepoSchema; @@ -27,7 +27,7 @@ use super::{ }; #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema(base_name = "list", folder_path = "outputs/resource")] pub struct DscResource { /// The namespaced name of the resource. @@ -39,6 +39,8 @@ pub struct DscResource { pub version: String, /// The capabilities of the resource. pub capabilities: Vec, + /// An optional message indicating the resource is deprecated. If provided, the message will be shown when the resource is used. + pub deprecation_message: Option, /// The file path to the resource. pub path: PathBuf, /// The description of the resource. @@ -46,14 +48,12 @@ pub struct DscResource { // The directory path to the resource. pub directory: PathBuf, /// The implementation of the resource. - #[serde(rename="implementedAs")] pub implemented_as: Option, /// The author of the resource. pub author: Option, /// The properties of the resource. pub properties: Option>, /// The required resource adapter for the resource. - #[serde(rename="requireAdapter")] pub require_adapter: Option, /// The JSON Schema of the resource. pub schema: Option>, @@ -103,6 +103,7 @@ impl DscResource { kind: Kind::Resource, version: String::new(), capabilities: Vec::new(), + deprecation_message: None, description: None, path: PathBuf::new(), directory: PathBuf::new(), @@ -383,6 +384,9 @@ pub trait Invoke { impl Invoke for DscResource { fn get(&self, filter: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeGet", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if let Some(adapter) = &self.require_adapter { return self.invoke_get_with_adapter(adapter, &self, filter); } @@ -399,6 +403,9 @@ impl Invoke for DscResource { fn set(&self, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { debug!("{}", t!("dscresources.dscresource.invokeSet", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if let Some(adapter) = &self.require_adapter { return self.invoke_set_with_adapter(adapter, &self, desired, skip_test, execution_type); } @@ -415,6 +422,9 @@ impl Invoke for DscResource { fn test(&self, expected: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeTest", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if let Some(adapter) = &self.require_adapter { return self.invoke_test_with_adapter(adapter, &self, expected); } @@ -463,6 +473,9 @@ impl Invoke for DscResource { fn delete(&self, filter: &str, execution_type: &ExecutionKind) -> Result { debug!("{}", t!("dscresources.dscresource.invokeDelete", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if let Some(adapter) = &self.require_adapter { return self.invoke_delete_with_adapter(adapter, &self, filter, execution_type); } @@ -479,6 +492,9 @@ impl Invoke for DscResource { fn validate(&self, config: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeValidate", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if self.require_adapter.is_some() { return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeValidateNotSupported", resource = self.type_name).to_string())); } @@ -495,6 +511,9 @@ impl Invoke for DscResource { fn schema(&self) -> Result { debug!("{}", t!("dscresources.dscresource.invokeSchema", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if self.require_adapter.is_some() { return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeSchemaNotSupported", resource = self.type_name).to_string())); } @@ -511,6 +530,9 @@ impl Invoke for DscResource { fn export(&self, input: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeExport", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if let Some(adapter) = &self.require_adapter { return self.invoke_export_with_adapter(adapter, &self, input); } @@ -520,6 +542,9 @@ impl Invoke for DscResource { fn resolve(&self, input: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeResolve", resource = self.type_name)); + if let Some(deprecation_message) = self.deprecation_message.as_ref() { + warn!("{}", t!("dscresources.dscresource.deprecationMessage", resource = self.type_name, message = deprecation_message)); + } if self.require_adapter.is_some() { return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeResolveNotSupported", resource = self.type_name).to_string())); } diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index 855a54518..ba0b1d859 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -25,7 +25,7 @@ pub enum Kind { } #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( base_name = "manifest", folder_path = "resource", @@ -47,12 +47,16 @@ pub struct ResourceManifest { /// An optional condition for the resource to be active. If the condition evaluates to false, the resource is skipped. #[serde(skip_serializing_if = "Option::is_none")] pub condition: Option, + /// An optional message indicating the resource is deprecated. If provided, the message will be shown when the resource is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecation_message: Option, /// The kind of resource. #[serde(skip_serializing_if = "Option::is_none")] pub kind: Option, /// The version of the resource using semantic versioning. pub version: String, /// The description of the resource. + #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// Tags for the resource. #[serde(default, skip_serializing_if = "TagList::is_empty")] @@ -63,7 +67,7 @@ pub struct ResourceManifest { #[serde(skip_serializing_if = "Option::is_none")] pub set: Option, /// Details how to call the `WhatIf` method of the resource. - #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub what_if: Option, /// Details how to call the Test method of the resource. #[serde(skip_serializing_if = "Option::is_none")] @@ -84,7 +88,7 @@ pub struct ResourceManifest { #[serde(skip_serializing_if = "Option::is_none")] pub adapter: Option, /// Mapping of exit codes to descriptions. Zero is always success and non-zero is always failure. - #[serde(rename = "exitCodes", skip_serializing_if = "ExitCodesMap::is_empty_or_default", default)] + #[serde(skip_serializing_if = "ExitCodesMap::is_empty_or_default", default)] pub exit_codes: ExitCodesMap, /// Details how to get the schema of the resource. #[serde(skip_serializing_if = "Option::is_none")] diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index cb3c63d6e..42549328b 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -26,7 +26,7 @@ use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::path::PathBuf; -use tracing::{info, trace}; +use tracing::{info, trace, warn}; #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[dsc_repo_schema(base_name = "manifest.discover", folder_path = "extension")] @@ -73,6 +73,9 @@ impl DscExtension { path: None, }; let args = process_get_args(discover.args.as_ref(), "", &command_resource_info); + if let Some(deprecation_message) = extension.deprecation_message.as_ref() { + warn!("{}", t!("extensions.dscextension.deprecationMessage", extension = self.type_name, message = deprecation_message)); + } let (_exit_code, stdout, _stderr) = invoke_command( &discover.executable, args, diff --git a/lib/dsc-lib/src/extensions/dscextension.rs b/lib/dsc-lib/src/extensions/dscextension.rs index ad57a9762..8c4d65e22 100644 --- a/lib/dsc-lib/src/extensions/dscextension.rs +++ b/lib/dsc-lib/src/extensions/dscextension.rs @@ -14,24 +14,26 @@ use std::path::PathBuf; #[serde(deny_unknown_fields)] #[dsc_repo_schema(base_name = "list", folder_path = "outputs/extension")] pub struct DscExtension { - /// The namespaced name of the resource. + /// The namespaced name of the extension. #[serde(rename="type")] pub type_name: FullyQualifiedTypeName, - /// The version of the resource. + /// The version of the extension. pub version: String, - /// The capabilities of the resource. + /// The capabilities of the extension. pub capabilities: Vec, /// The import specifics. pub import: Option, - /// The file path to the resource. + /// The file path to the extension. pub path: PathBuf, - /// The description of the resource. + /// An optional message indicating the extension is deprecated. If provided, the message will be shown when the extension is used. + pub deprecation_message: Option, + /// The description of the extension. pub description: Option, - // The directory path to the resource. + // The directory path to the extension. pub directory: PathBuf, - /// The author of the resource. + /// The author of the extension. pub author: Option, - /// The manifest of the resource. + /// The manifest of the extension. pub manifest: Value, } @@ -66,6 +68,7 @@ impl DscExtension { version: String::new(), capabilities: Vec::new(), import: None, + deprecation_message: None, description: None, path: PathBuf::new(), directory: PathBuf::new(), diff --git a/lib/dsc-lib/src/extensions/extension_manifest.rs b/lib/dsc-lib/src/extensions/extension_manifest.rs index 8626b9738..a799537b2 100644 --- a/lib/dsc-lib/src/extensions/extension_manifest.rs +++ b/lib/dsc-lib/src/extensions/extension_manifest.rs @@ -13,7 +13,7 @@ use crate::schemas::dsc_repo::DscRepoSchema; use crate::types::{ExitCodesMap, FullyQualifiedTypeName, TagList}; #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( base_name = "manifest", folder_path = "extension", @@ -37,6 +37,9 @@ pub struct ExtensionManifest { /// An optional condition for the extension to be active. If the condition evaluates to false, the extension is skipped. #[serde(skip_serializing_if = "Option::is_none")] pub condition: Option, + /// An optional message indicating the extension is deprecated. If provided, the message will be shown when the extension is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub deprecation_message: Option, /// The description of the extension. pub description: Option, /// Tags for the extension. @@ -47,18 +50,17 @@ pub struct ExtensionManifest { /// Details how to call the Import method of the extension. pub import: Option, /// Details how to call the ImportParameters method of the extension. - #[serde(rename = "importParameters")] pub import_parameters: Option, /// Details how to call the Secret method of the extension. pub secret: Option, /// Mapping of exit codes to descriptions. Zero is always success and non-zero is always failure. - #[serde(rename = "exitCodes", skip_serializing_if = "ExitCodesMap::is_empty_or_default", default)] + #[serde(skip_serializing_if = "ExitCodesMap::is_empty_or_default", default)] pub exit_codes: ExitCodesMap, #[serde(skip_serializing_if = "Option::is_none")] pub metadata: Option>, } -/// Import a resource manifest from a JSON value. +/// Import an extension manifest from a JSON value. /// /// # Arguments /// @@ -66,7 +68,7 @@ pub struct ExtensionManifest { /// /// # Returns /// -/// * `Result` - The imported resource manifest. +/// * `Result` - The imported extension manifest. /// /// # Errors /// diff --git a/lib/dsc-lib/src/extensions/import.rs b/lib/dsc-lib/src/extensions/import.rs index 661987828..0378f8844 100644 --- a/lib/dsc-lib/src/extensions/import.rs +++ b/lib/dsc-lib/src/extensions/import.rs @@ -18,7 +18,7 @@ use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::path::Path; -use tracing::{debug, info}; +use tracing::{debug, info, warn}; #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[dsc_repo_schema(base_name = "manifest.import", folder_path = "extension")] @@ -82,6 +82,9 @@ impl DscExtension { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Import.to_string())); }; let args = process_import_args(import.args.as_ref(), file)?; + if let Some(deprecation_message) = extension.deprecation_message.as_ref() { + warn!("{}", t!("extensions.dscextension.deprecationMessage", extension = self.type_name, message = deprecation_message)); + } let (_exit_code, stdout, _stderr) = invoke_command( &import.executable, args, diff --git a/lib/dsc-lib/src/extensions/secret.rs b/lib/dsc-lib/src/extensions/secret.rs index 2805e2588..bd9892e98 100644 --- a/lib/dsc-lib/src/extensions/secret.rs +++ b/lib/dsc-lib/src/extensions/secret.rs @@ -19,7 +19,7 @@ use crate::{ use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use tracing::debug; +use tracing::{debug, warn}; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(untagged)] @@ -77,6 +77,9 @@ impl DscExtension { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Secret.to_string())); }; let args = process_secret_args(secret.args.as_ref(), name, vault); + if let Some(deprecation_message) = extension.deprecation_message.as_ref() { + warn!("{}", t!("extensions.dscextension.deprecationMessage", extension = self.type_name, message = deprecation_message)); + } let (_exit_code, stdout, _stderr) = invoke_command( &secret.executable, args, @@ -105,7 +108,7 @@ impl DscExtension { Capability::Secret.to_string() )) } - } + } } fn process_secret_args(args: Option<&Vec>, name: &str, vault: Option<&str>) -> Option> { diff --git a/tools/dsctest/.project.data.json b/tools/dsctest/.project.data.json index 8f7300723..1da7f9865 100644 --- a/tools/dsctest/.project.data.json +++ b/tools/dsctest/.project.data.json @@ -9,7 +9,8 @@ "CopyFiles": { "All": [ "adaptedTest.dsc.adaptedResource.json", - "dsctest.dsc.manifests.json" + "dsctest.dsc.manifests.json", + "deprecated/deprecated.dsc.manifests.json" ] } } diff --git a/tools/dsctest/deprecated/deprecated.dsc.manifests.json b/tools/dsctest/deprecated/deprecated.dsc.manifests.json new file mode 100644 index 000000000..cb0c96f45 --- /dev/null +++ b/tools/dsctest/deprecated/deprecated.dsc.manifests.json @@ -0,0 +1,147 @@ +{ + "adaptedResources": [ + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json", + "type": "Adapted/Deprecated", + "deprecationMessage": "This adapted resource is deprecated", + "kind": "resource", + "version": "1.0.0", + "capabilities": [ + "get", + "set", + "test", + "export" + ], + "description": "An adapted resource for testing.", + "author": "DSC Team", + "requireAdapter": "Test/Adapter", + "path": "deprecated.dsc.manifests.json", + "schema": { + "embedded": { + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Microsoft/OSInfo/v0.1.0/schema.json", + "title": "OsInfo", + "description": "Returns information about the operating system.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft/osinfo/resource\n", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "two": { + "type": "string", + "title": "Property Two", + "description": "This is property two of the adapted resource." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the adapted resource instance." + } + } + } + } + } + ], + "extensions": [ + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/ExtensionDeprecated", + "version": "0.1.0", + "deprecationMessage": "This extension is deprecated", + "description": "Test extension", + "discover": { + "executable": "dsctest", + "args": [ + "no-op" + ] + }, + "import": { + "executable": "dsctest", + "args": [ + "no-op" + ], + "fileExtensions": [ + "testimport" + ] + }, + "secret": { + "executable": "dsctest", + "args": [ + "no-op" + ] + } + } + ], + "resources": [ + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/OperationDeprecated", + "deprecationMessage": "This resource is deprecated", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "get", + { + "jsonInputArg": "--input" + } + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "set", + { + "jsonInputArg": "--input" + } + ] + }, + "test": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "trace", + { + "jsonInputArg": "--input" + } + ] + }, + "delete": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "delete", + { + "jsonInputArg": "--input" + } + ] + }, + "export": { + "executable": "dsctest", + "args": [ + "operation", + "--operation", + "export", + { + "jsonInputArg": "--input" + } + ] + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "operation" + ] + } + } + } + ] +} diff --git a/tools/dsctest/src/adapter.rs b/tools/dsctest/src/adapter.rs index 6672dde02..48806d805 100644 --- a/tools/dsctest/src/adapter.rs +++ b/tools/dsctest/src/adapter.rs @@ -107,6 +107,14 @@ pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation, res }; Ok(serde_json::to_string(&adapted_three).unwrap()) }, + "Adapted/Deprecated" => { + let adapted_deprecated = AdaptedOne { + one: "deprecated".to_string(), + name: None, + path: resource_path.clone(), + }; + Ok(serde_json::to_string(&adapted_deprecated).unwrap()) + }, _ => Err(format!("Unknown resource type: {resource_type}")), } }, diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 11232cc2f..21c4f4d1e 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -111,6 +111,9 @@ pub enum SubCommand { export: bool, }, + #[clap(name = "no-op", about = "Perform no operation, just return success")] + NoOp, + #[clap(name = "operation", about = "Perform an operation")] Operation { #[clap(name = "operation", short, long, help = "The name of the operation to perform")] diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 43b043053..5da8dc48a 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -229,6 +229,10 @@ fn main() { } String::new() }, + SubCommand::NoOp => { + // do nothing and just return success + String::new() + }, SubCommand::Operation { operation, input } => { let mut operation_result = match serde_json::from_str::(&input) { Ok(op) => op, diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index 219357038..e6ed15de7 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -19,6 +19,7 @@ fn main() { kind: Kind::Resource, version: "1.0.0".to_string(), capabilities: vec![Capability::Get, Capability::Set], + deprecation_message: None, description: Some("This is a test resource.".to_string()), implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), path: PathBuf::from("test_resource1"), @@ -46,6 +47,7 @@ fn main() { kind: Kind::Resource, version: "1.0.1".to_string(), capabilities: vec![Capability::Get, Capability::Set], + deprecation_message: None, description: Some("This is a test resource.".to_string()), implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), path: PathBuf::from("test_resource2"), @@ -77,6 +79,7 @@ fn main() { kind: Kind::Resource, version: "1.0.0".to_string(), capabilities: vec![Capability::Get], + deprecation_message: None, description: Some("This is a test resource.".to_string()), implemented_as: Some(ImplementedAs::Custom("TestResource".to_string())), path: PathBuf::from("test_resource1"),