diff --git a/build.helpers.psm1 b/build.helpers.psm1 index a9d50525c..f7cd8eaf9 100644 --- a/build.helpers.psm1 +++ b/build.helpers.psm1 @@ -651,7 +651,14 @@ function Install-Protobuf { } elseif ($IsWindows) { if (Test-CommandAvailable -Name 'winget') { Write-Verbose -Verbose "Using winget to install Protobuf" - winget install Google.Protobuf --accept-source-agreements --accept-package-agreements --source winget --silent + winget install Google.Protobuf --accept-source-agreements --accept-package-agreements --source winget --force + # need to add to PATH + $protocFolder = "$env:USERPROFILE\AppData\Local\Microsoft\WinGet\Packages\Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe\bin" + if (Test-Path $protocFolder) { + $env:PATH += ";$protocFolder" + } else { + throw "protoc folder not found after installation: $protocFolder" + } } else { Write-Warning "winget not found, please install Protobuf manually" } @@ -665,7 +672,7 @@ function Install-Protobuf { } if ($LASTEXITCODE -ne 0) { - throw "Failed to install Protobuf" + throw "Failed to install Protobuf: $LASTEXITCODE" } } } diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 792a48a5d..3e750fa03 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -3,8 +3,6 @@ Describe 'tests for resource discovery' { BeforeAll { - $env:DSC_RESOURCE_PATH = $testdrive - $script:lookupTableFilePath = if ($IsWindows) { Join-Path $env:LocalAppData "dsc\AdaptedResourcesLookupTable.json" } else { @@ -16,10 +14,6 @@ Describe 'tests for resource discovery' { Remove-Item -Path "$testdrive/test.dsc.resource.*" -ErrorAction SilentlyContinue } - AfterAll { - $env:DSC_RESOURCE_PATH = $null - } - It 'Use DSC_RESOURCE_PATH instead of PATH when defined' { $resourceJson = @' { @@ -31,75 +25,85 @@ Describe 'tests for resource discovery' { } } '@ - - Set-Content -Path "$testdrive/test.dsc.resource.json" -Value $resourceJson - $resources = dsc resource list | ConvertFrom-Json - $resources.Count | Should -Be 1 - $resources.type | Should -BeExactly 'DSC/TestPathResource' + try { + $oldPath = $env:PATH + $env:DSC_RESOURCE_PATH = $testdrive + Set-Content -Path "$testdrive/test.dsc.resource.json" -Value $resourceJson + $resources = dsc resource list | ConvertFrom-Json + $resources.Count | Should -Be 1 + $resources.type | Should -BeExactly 'DSC/TestPathResource' + } + finally { + $env:PATH = $oldPath + $env:DSC_RESOURCE_PATH = $null + } } - It 'support discovering ' -TestCases @( - @{ extension = 'yaml' } - @{ extension = 'yml' } - ) { - param($extension) - - $resourceYaml = @' - $schema: https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json - type: DSC/TestYamlResource - version: 0.1.0 - get: - executable: dsc + Context 'Forced discovery using $testdrive' { + BeforeAll { + $env:DSC_RESOURCE_PATH = $testdrive + } + + AfterAll { + $env:DSC_RESOURCE_PATH = $null + } + + It 'support discovering ' -TestCases @( + @{ extension = 'yaml' } + @{ extension = 'yml' } + ) { + param($extension) + + $resourceYaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json + type: DSC/TestYamlResource + version: 0.1.0 + get: + executable: dsc '@ - Set-Content -Path "$testdrive/test.dsc.resource.$extension" -Value $resourceYaml - $resources = dsc resource list | ConvertFrom-Json - $resources.Count | Should -Be 1 - $resources.type | Should -BeExactly 'DSC/TestYamlResource' - } + Set-Content -Path "$testdrive/test.dsc.resource.$extension" -Value $resourceYaml + $resources = dsc resource list | ConvertFrom-Json + $resources.Count | Should -Be 1 + $resources.type | Should -BeExactly 'DSC/TestYamlResource' + } - It 'does not support discovering a file with an extension that is not json or yaml' { - param($extension) + It 'does not support discovering a file with an extension that is not json or yaml' { + param($extension) - $resourceInput = @' - $schema: https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json - type: DSC/TestYamlResource - version: 0.1.0 - get: - executable: dsc + $resourceInput = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json + type: DSC/TestYamlResource + version: 0.1.0 + get: + executable: dsc '@ - Set-Content -Path "$testdrive/test.dsc.resource.txt" -Value $resourceInput - $resources = dsc resource list | ConvertFrom-Json - $resources.Count | Should -Be 0 - } + Set-Content -Path "$testdrive/test.dsc.resource.txt" -Value $resourceInput + $resources = dsc resource list | ConvertFrom-Json + $resources.Count | Should -Be 0 + } - It 'warns on invalid semver' { - $manifest = @' - { - "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", - "type": "Test/InvalidSemver", - "version": "1.1.0..1", - "get": { - "executable": "dsctest" - }, - "schema": { - "command": { + It 'warns on invalid semver' { + $manifest = @' + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/InvalidSemver", + "version": "1.1.0..1", + "get": { "executable": "dsctest" + }, + "schema": { + "command": { + "executable": "dsctest" + } } } - } '@ - $oldPath = $env:DSC_RESOURCE_PATH - try { - $env:DSC_RESOURCE_PATH = $testdrive Set-Content -Path "$testdrive/test.dsc.resource.json" -Value $manifest $null = dsc resource list 2> "$testdrive/error.txt" "$testdrive/error.txt" | Should -FileContentMatchExactly 'WARN.*?does not use semver' -Because (Get-Content -Raw "$testdrive/error.txt") } - finally { - $env:DSC_RESOURCE_PATH = $oldPath - } } It 'Ensure List operation populates adapter lookup table' { @@ -296,4 +300,47 @@ Describe 'tests for resource discovery' { $env:DSC_RESOURCE_PATH = $null } } + + It 'Resource discovery can be set to ' -TestCases @( + @{ namespace = 'Microsoft.DSC'; mode = 'preDeployment' } + @{ namespace = 'Microsoft.DSC'; mode = 'duringDeployment' } + @{ namespace = 'Ignore'; mode = 'ignore' } + ) { + param($namespace, $mode) + + $guid = (New-Guid).Guid.Replace('-', '') + $manifestPath = Join-Path (Split-Path (Get-Command dscecho -ErrorAction Stop).Source -Parent) echo.dsc.resource.json + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/manifest.json + metadata: + ${namespace}: + resourceDiscovery: $mode + resources: + - type: Test/CopyResource + name: This should be found and executed + properties: + sourceFile: $manifestPath + typeName: "Test/$guid" + - type: Test/$guid + name: This is the new resource + properties: + output: Hello World +"@ + $out = dsc -l trace config get -i $config_yaml 2> "$testdrive/tracing.txt" + $traceLog = Get-Content -Raw -Path "$testdrive/tracing.txt" + if ($mode -ne 'duringDeployment') { + $LASTEXITCODE | Should -Be 2 + $out | Should -BeNullOrEmpty + $traceLog | Should -Match "ERROR.*?Resource not found: Test/$guid" + $traceLog | Should -Not -Match "Invoking get for 'Test/CopyResource'" + } else { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path "$testdrive/tracing.txt") + $output = $out | ConvertFrom-Json + $output.results[0].result.actualState.typeName | Should -BeExactly "Test/$guid" -Because $out + $output.results[1].result.actualState.output | Should -BeExactly 'Hello World' -Because $out + $traceLog | Should -Match "Invoking get for 'Test/$guid'" + $traceLog | Should -Match "Skipping resource discovery due to 'resourceDiscovery' mode set to 'DuringDeployment'" + } + } } diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 6291919c4..fc8523cf6 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -84,6 +84,7 @@ skippingOutput = "Skipping output for '%{name}' due to condition evaluating to f secureOutputSkipped = "Secure output '%{name}' is skipped" outputTypeNotMatch = "Output '%{name}' type does not match expected type '%{expected_type}'" copyNotSupported = "Copy for output '%{name}' is currently not supported" +skippingResourceDiscovery = "Skipping resource discovery due to 'resourceDiscovery' mode set to 'DuringDeployment'" [configure.parameters] importingParametersFromComplexInput = "Importing parameters from complex input" diff --git a/lib/dsc-lib/src/configure/config_doc.rs b/lib/dsc-lib/src/configure/config_doc.rs index 3d0bfcf88..fd3eb680e 100644 --- a/lib/dsc-lib/src/configure/config_doc.rs +++ b/lib/dsc-lib/src/configure/config_doc.rs @@ -60,34 +60,46 @@ pub enum RestartRequired { Process(Process), } +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema)] +#[serde(rename_all = "camelCase")] +#[dsc_repo_schema(base_name = "resourceDiscovery", folder_path = "metadata/Microsoft.DSC")] +pub enum ResourceDiscoveryMode { + PreDeployment, + DuringDeployment, +} + #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] pub struct MicrosoftDscMetadata { /// The duration of the configuration operation #[serde(skip_serializing_if = "Option::is_none")] pub duration: Option, /// The end time of the configuration operation - #[serde(rename = "endDatetime", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub end_datetime: Option, /// The type of execution - #[serde(rename = "executionType", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub execution_type: Option, /// The operation being performed #[serde(skip_serializing_if = "Option::is_none")] pub operation: Option, /// Specify specific adapter type used for implicit operations - #[serde(rename = "requireAdapter", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub require_adapter: Option, + /// Indicates if resources are discovered pre-deployment or during deployment + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_discovery: Option, /// Indicates what needs to be restarted after the configuration operation - #[serde(rename = "restartRequired", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub restart_required: Option>, /// Copy loop context for resources expanded from copy loops #[serde(rename = "copyLoops", skip_serializing_if = "Option::is_none")] pub copy_loops: Option>, /// The security context of the configuration operation, can be specified to be required - #[serde(rename = "securityContext", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub security_context: Option, /// The start time of the configuration operation - #[serde(rename = "startDatetime", skip_serializing_if = "Option::is_none")] + #[serde(skip_serializing_if = "Option::is_none")] pub start_datetime: Option, /// Version of DSC #[serde(skip_serializing_if = "Option::is_none")] diff --git a/lib/dsc-lib/src/configure/mod.rs b/lib/dsc-lib/src/configure/mod.rs index e0c1f66d4..0be52c23b 100644 --- a/lib/dsc-lib/src/configure/mod.rs +++ b/lib/dsc-lib/src/configure/mod.rs @@ -3,7 +3,7 @@ use crate::configure::context::{Context, ProcessMode}; use crate::configure::parameters::import_parameters; -use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, RestartRequired, ValueOrCopy}}; +use crate::configure::{config_doc::{ExecutionKind, IntOrExpression, Metadata, Parameter, Resource, ResourceDiscoveryMode, RestartRequired, ValueOrCopy}}; use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; use crate::dscresources::{ @@ -998,6 +998,7 @@ impl Configurator { end_datetime: Some(end_datetime.to_rfc3339()), execution_type: Some(self.context.execution_type.clone()), operation: Some(operation), + resource_discovery: None, restart_required: self.context.restart_required.clone(), security_context: Some(self.context.security_context.clone()), start_datetime: Some(self.context.start_datetime.to_rfc3339()), @@ -1013,29 +1014,52 @@ impl Configurator { let config: Configuration = serde_json::from_str(self.json.as_str())?; check_security_context(config.metadata.as_ref())?; - // Perform discovery of resources used in config - // create an array of DiscoveryFilter using the resource types and api_versions from the config - let mut discovery_filter: Vec = Vec::new(); - let config_copy = config.clone(); - for resource in config_copy.resources { - let adapter = get_require_adapter_from_metadata(&resource.metadata); - let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()); - if !discovery_filter.contains(&filter) { - discovery_filter.push(filter); + let mut skip_resource_validation = false; + if let Some(metadata) = &config.metadata { + if let Some(microsoft_metadata) = &metadata.microsoft { + if let Some(mode) = µsoft_metadata.resource_discovery { + if *mode == ResourceDiscoveryMode::DuringDeployment { + debug!("{}", t!("configure.mod.skippingResourceDiscovery")); + skip_resource_validation = true; + self.discovery.refresh_cache = true; + } + } } - // defer actual unrolling until parameters are available - if let Some(copy) = &resource.copy { - debug!("{}", t!("configure.mod.validateCopy", name = ©.name, count = copy.count)); - if copy.mode.is_some() { - return Err(DscError::Validation(t!("configure.mod.copyModeNotSupported").to_string())); + } + + if !skip_resource_validation { + // Perform discovery of resources used in config + // create an array of DiscoveryFilter using the resource types and api_versions from the config + let mut discovery_filter: Vec = Vec::new(); + let config_copy = config.clone(); + for resource in config_copy.resources { + let adapter = get_require_adapter_from_metadata(&resource.metadata); + let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()); + if !discovery_filter.contains(&filter) { + discovery_filter.push(filter); } - if copy.batch_size.is_some() { - return Err(DscError::Validation(t!("configure.mod.copyBatchSizeNotSupported").to_string())); + // defer actual unrolling until parameters are available + if let Some(copy) = &resource.copy { + debug!("{}", t!("configure.mod.validateCopy", name = ©.name, count = copy.count)); + if copy.mode.is_some() { + return Err(DscError::Validation(t!("configure.mod.copyModeNotSupported").to_string())); + } + if copy.batch_size.is_some() { + return Err(DscError::Validation(t!("configure.mod.copyBatchSizeNotSupported").to_string())); + } } } + self.discovery.find_resources(&discovery_filter, self.progress_format)?; + + // now check that each resource in the config was found + for resource in config.resources.iter() { + let adapter = get_require_adapter_from_metadata(&resource.metadata); + let Some(_dsc_resource) = self.discovery.find_resource(&DiscoveryFilter::new(&resource.resource_type, resource.api_version.as_deref(), adapter.as_deref()))? else { + return Err(DscError::ResourceNotFound(resource.resource_type.to_string(), resource.api_version.as_deref().unwrap_or("").to_string())); + }; + } } - self.discovery.find_resources(&discovery_filter, self.progress_format)?; self.config = config; Ok(()) } diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index a10768a85..2be6f6dd3 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -2,8 +2,8 @@ // Licensed under the MIT License. use crate::{discovery::{discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, matches_adapter_requirement}, parser::Statement}; -use crate::{locked_is_empty, locked_extend, locked_clone, locked_get}; -use crate::configure::context::Context; +use crate::{locked_clear, locked_is_empty, locked_extend, locked_clone, locked_get}; +use crate::configure::{config_doc::ResourceDiscoveryMode, context::Context}; use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest, SchemaKind}; use crate::dscresources::command_resource::invoke_command; @@ -56,6 +56,7 @@ pub enum ImportedManifest { #[derive(Clone)] pub struct CommandDiscovery { progress_format: ProgressFormat, + discovery_mode: ResourceDiscoveryMode, } #[derive(Deserialize)] @@ -85,6 +86,7 @@ impl CommandDiscovery { pub fn new(progress_format: ProgressFormat) -> CommandDiscovery { CommandDiscovery { progress_format, + discovery_mode: ResourceDiscoveryMode::PreDeployment, } } @@ -205,12 +207,15 @@ impl ResourceDiscovery for CommandDiscovery { #[allow(clippy::too_many_lines)] fn discover(&mut self, kind: &DiscoveryKind, filter: &str) -> Result<(), DscError> { - if !locked_is_empty!(RESOURCES) { + if self.discovery_mode == ResourceDiscoveryMode::PreDeployment && !locked_is_empty!(RESOURCES) { return Ok(()); + } else if self.discovery_mode == ResourceDiscoveryMode::DuringDeployment { + locked_clear!(RESOURCES); + locked_clear!(ADAPTERS); } // if kind is DscResource, we need to discover extensions first - if *kind == DiscoveryKind::Resource && locked_is_empty!(EXTENSIONS) { + if *kind == DiscoveryKind::Resource && (self.discovery_mode == ResourceDiscoveryMode::DuringDeployment || locked_is_empty!(EXTENSIONS)){ self.discover(&DiscoveryKind::Extension, "*")?; } @@ -351,7 +356,7 @@ impl ResourceDiscovery for CommandDiscovery { } fn discover_adapted_resources(&mut self, name_filter: &str, adapter_filter: &str) -> Result<(), DscError> { - if locked_is_empty!(RESOURCES) && locked_is_empty!(ADAPTERS) { + if self.discovery_mode == ResourceDiscoveryMode::DuringDeployment || (locked_is_empty!(RESOURCES) && locked_is_empty!(ADAPTERS)) { self.discover(&DiscoveryKind::Resource, "*")?; } @@ -490,7 +495,7 @@ impl ResourceDiscovery for CommandDiscovery { fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter]) -> Result>, DscError> { debug!("{}", t!("discovery.commandDiscovery.searchingForResources", resources = required_resource_types : {:?})); - if locked_is_empty!(RESOURCES) { + if self.discovery_mode == ResourceDiscoveryMode::DuringDeployment || locked_is_empty!(RESOURCES) { self.discover(&DiscoveryKind::Resource, "*")?; } let mut found_resources = BTreeMap::>::new(); @@ -553,6 +558,10 @@ impl ResourceDiscovery for CommandDiscovery { } Ok(locked_clone!(EXTENSIONS)) } + + fn set_discovery_mode(&mut self, mode: &ResourceDiscoveryMode) { + self.discovery_mode = mode.clone(); + } } fn filter_resources(found_resources: &mut BTreeMap>, required_resources: &mut HashMap, resources: &[DscResource], filter: &DiscoveryFilter) { diff --git a/lib/dsc-lib/src/discovery/discovery_trait.rs b/lib/dsc-lib/src/discovery/discovery_trait.rs index d9a557071..ddc007198 100644 --- a/lib/dsc-lib/src/discovery/discovery_trait.rs +++ b/lib/dsc-lib/src/discovery/discovery_trait.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{dscerror::DscError, dscresources::dscresource::DscResource, extensions::dscextension::DscExtension}; +use crate::{configure::config_doc::ResourceDiscoveryMode, dscerror::DscError, dscresources::dscresource::DscResource, extensions::dscextension::DscExtension}; use std::collections::BTreeMap; use super::{command_discovery::ImportedManifest, fix_semver}; @@ -121,4 +121,11 @@ pub trait ResourceDiscovery { /// /// This function will return an error if the underlying discovery fails. fn get_extensions(&mut self) -> Result, DscError>; + + /// Set the discovery mode. + /// + /// # Arguments + /// + /// * `mode` - The resource discovery mode to set. + fn set_discovery_mode(&mut self, mode: &ResourceDiscoveryMode); } diff --git a/lib/dsc-lib/src/discovery/mod.rs b/lib/dsc-lib/src/discovery/mod.rs index 7e5069cd1..38c76113f 100644 --- a/lib/dsc-lib/src/discovery/mod.rs +++ b/lib/dsc-lib/src/discovery/mod.rs @@ -4,6 +4,7 @@ pub mod command_discovery; pub mod discovery_trait; +use crate::configure::config_doc::ResourceDiscoveryMode; use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery, DiscoveryFilter}; use crate::dscerror::DscError; use crate::extensions::dscextension::{Capability, DscExtension}; @@ -18,6 +19,7 @@ use tracing::error; pub struct Discovery { pub resources: BTreeMap>, pub extensions: BTreeMap, + pub refresh_cache: bool, } impl Discovery { @@ -32,6 +34,7 @@ impl Discovery { Self { resources: BTreeMap::new(), extensions: BTreeMap::new(), + refresh_cache: false, } } @@ -89,7 +92,7 @@ impl Discovery { #[must_use] pub fn find_resource(&mut self, filter: &DiscoveryFilter) -> Result, DscError> { - if self.resources.is_empty() { + if self.refresh_cache || self.resources.is_empty() { self.find_resources(&[filter.clone()], ProgressFormat::None)?; } @@ -133,12 +136,17 @@ impl Discovery { /// /// * `required_resource_types` - The required resource types. pub fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter], progress_format: ProgressFormat) -> Result<(), DscError> { - if !self.resources.is_empty() { + if !self.refresh_cache && !self.resources.is_empty() { // If resources are already discovered, no need to re-discover. return Ok(()); } - let command_discovery = CommandDiscovery::new(progress_format); + let mut command_discovery = CommandDiscovery::new(progress_format); + if self.refresh_cache { + self.resources.clear(); + self.extensions.clear(); + command_discovery.set_discovery_mode(&ResourceDiscoveryMode::DuringDeployment); + } let discovery_types: Vec> = vec![ Box::new(command_discovery), ]; @@ -158,11 +166,11 @@ impl Discovery { } /// Check if a resource matches the adapter requirement specified in the filter. -/// +/// /// # Arguments /// * `resource` - The resource to check. /// * `filter` - The discovery filter containing the adapter requirement. -/// +/// /// # Returns /// `true` if the resource matches the adapter requirement, `false` otherwise. pub fn matches_adapter_requirement(resource: &DscResource, filter: &DiscoveryFilter) -> bool { diff --git a/lib/dsc-lib/src/util.rs b/lib/dsc-lib/src/util.rs index 086076cd6..ba68e031c 100644 --- a/lib/dsc-lib/src/util.rs +++ b/lib/dsc-lib/src/util.rs @@ -251,6 +251,13 @@ pub fn canonicalize_which(executable: &str, cwd: Option<&Path>) -> Result {{ + $lockable.write().unwrap().clear(); + }}; +} + #[macro_export] macro_rules! locked_is_empty { ($lockable:expr) => {{ diff --git a/tools/dsctest/dsctest.dsc.manifests.json b/tools/dsctest/dsctest.dsc.manifests.json index e790c1328..b5afea1fc 100644 --- a/tools/dsctest/dsctest.dsc.manifests.json +++ b/tools/dsctest/dsctest.dsc.manifests.json @@ -1,5 +1,30 @@ { "resources": [ + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/CopyResource", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "copy-resource", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "copy-resource" + ] + } + } + }, { "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "type": "Test/Delete", diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index ed896dbc8..6bc62da6e 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -6,6 +6,7 @@ use clap::{Parser, Subcommand, ValueEnum}; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] pub enum Schemas { Adapter, + CopyResource, Delete, Exist, ExitCode, @@ -51,6 +52,12 @@ pub enum SubCommand { operation: AdapterOperation, }, + #[clap(name = "copy-resource", about = "Copy a resource")] + CopyResource { + #[clap(name = "input", short, long, help = "The input to the copy resource command as JSON")] + input: String, + }, + #[clap(name = "delete", about = "delete operation")] Delete { #[clap(name = "input", short, long, help = "The input to the delete command as JSON")] diff --git a/tools/dsctest/src/copy_resource.rs b/tools/dsctest/src/copy_resource.rs new file mode 100644 index 000000000..a92d69623 --- /dev/null +++ b/tools/dsctest/src/copy_resource.rs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +pub struct CopyResource { + pub source_file: String, + pub type_name: String, +} + +pub fn copy_the_resource(source_file: &str, type_name: &str) -> Result<(), String> { + // open the source_file, deserialize it from JSON, change the `type` property to type_name, + // serialize it back to JSON and save it to a new file named ".dsc.resource.json" + let file_content = std::fs::read_to_string(source_file) + .map_err(|e| format!("Failed to read source file: {}", e))?; + let mut resource_json: serde_json::Value = serde_json::from_str(&file_content) + .map_err(|e| format!("Failed to parse JSON from source file: {}", e))?; + if let Some(obj) = resource_json.as_object_mut() { + obj.insert("type".to_string(), serde_json::Value::String(type_name.to_string())); + } else { + return Err("Source file not a resource manifest".to_string()); + } + let name_part = type_name.split('/').last().unwrap_or(type_name); + let output_file = format!("{name_part}.dsc.resource.json"); + let output_content = serde_json::to_string_pretty(&resource_json) + .map_err(|e| format!("Failed to serialize JSON: {e}"))?; + std::fs::write(&output_file, output_content) + .map_err(|e| format!("Failed to write output file: {e}"))?; + Ok(()) +} diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 04e2abd81..23bdf1fe9 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. mod args; +mod copy_resource; mod delete; mod exist; mod exit_code; @@ -21,6 +22,7 @@ use args::{Args, Schemas, SubCommand}; use clap::Parser; use schemars::schema_for; use serde_json::Map; +use crate::copy_resource::{CopyResource, copy_the_resource}; use crate::delete::Delete; use crate::exist::{Exist, State}; use crate::exit_code::ExitCode; @@ -49,6 +51,20 @@ fn main() { } } }, + SubCommand::CopyResource { input } => { + let copy_resource = match serde_json::from_str::(&input) { + Ok(copy_resource) => copy_resource, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + if let Err(err) = copy_the_resource(©_resource.source_file, ©_resource.type_name) { + eprintln!("Error copying resource: {err}"); + std::process::exit(1); + } + input + }, SubCommand::Delete { input } => { let mut delete = match serde_json::from_str::(&input) { Ok(delete) => delete, @@ -227,6 +243,9 @@ fn main() { Schemas::Adapter => { schema_for!(adapter::DscResource) }, + Schemas::CopyResource => { + schema_for!(CopyResource) + }, Schemas::Delete => { schema_for!(Delete) },