diff --git a/dsc/Cargo.lock b/dsc/Cargo.lock index ca31689a..50d44b9d 100644 --- a/dsc/Cargo.lock +++ b/dsc/Cargo.lock @@ -564,6 +564,7 @@ dependencies = [ "regex", "rust-i18n", "schemars", + "semver", "serde", "serde_json", "serde_yaml", diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index 63806fd7..dacb92c4 100644 --- a/dsc/Cargo.toml +++ b/dsc/Cargo.toml @@ -24,6 +24,7 @@ path-absolutize = { version = "3.1" } regex = "1.11" rust-i18n = { version = "3.1" } schemars = { version = "1.0" } +semver = "1.0" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } serde_yaml = { version = "0.9" } diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index d3089edf..4c8a08d6 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -34,6 +34,7 @@ getAll = "Get all instances of the resource" resource = "The name of the resource to invoke" functionAbout = "Operations on DSC functions" listFunctionAbout = "List or find functions" +version = "The version of the resource to invoke in semver format" [main] ctrlCReceived = "Ctrl-C received" diff --git a/dsc/src/args.rs b/dsc/src/args.rs index d929f1d0..01b6dbcf 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -215,6 +215,8 @@ pub enum ResourceSubCommand { all: bool, #[clap(short, long, help = t!("args.resource").to_string())] resource: String, + #[clap(short, long, help = t!("args.version").to_string())] + version: Option, #[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")] input: Option, #[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")] @@ -226,6 +228,8 @@ pub enum ResourceSubCommand { Set { #[clap(short, long, help = t!("args.resource").to_string())] resource: String, + #[clap(short, long, help = t!("args.version").to_string())] + version: Option, #[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")] input: Option, #[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")] @@ -237,6 +241,8 @@ pub enum ResourceSubCommand { Test { #[clap(short, long, help = t!("args.resource").to_string())] resource: String, + #[clap(short, long, help = t!("args.version").to_string())] + version: Option, #[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")] input: Option, #[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")] @@ -248,6 +254,8 @@ pub enum ResourceSubCommand { Delete { #[clap(short, long, help = t!("args.resource").to_string())] resource: String, + #[clap(short, long, help = t!("args.version").to_string())] + version: Option, #[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")] input: Option, #[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")] @@ -257,6 +265,8 @@ pub enum ResourceSubCommand { Schema { #[clap(short, long, help = t!("args.resource").to_string())] resource: String, + #[clap(short, long, help = t!("args.version").to_string())] + version: Option, #[clap(short = 'o', long, help = t!("args.outputFormat").to_string())] output_format: Option, }, @@ -264,6 +274,8 @@ pub enum ResourceSubCommand { Export { #[clap(short, long, help = t!("args.resource").to_string())] resource: String, + #[clap(short, long, help = t!("args.version").to_string())] + version: Option, #[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")] input: Option, #[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")] diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index c1b00f6f..22b6c7a7 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -16,9 +16,9 @@ use dsc_lib::{ }; use std::process::exit; -pub fn get(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&GetOutputFormat>) { - let Some(resource) = get_resource(dsc, resource_type) else { - error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); +pub fn get(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&GetOutputFormat>) { + let Some(resource) = get_resource(dsc, resource_type, version) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -67,10 +67,10 @@ pub fn get(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&G } } -pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&GetOutputFormat>) { +pub fn get_all(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, format: Option<&GetOutputFormat>) { let input = String::new(); - let Some(resource) = get_resource(dsc, resource_type) else { - error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); + let Some(resource) = get_resource(dsc, resource_type, version) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -125,14 +125,14 @@ pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&GetOutputF } } -pub fn set(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) { +pub fn set(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&OutputFormat>) { if input.is_empty() { error!("{}", t!("resource_command.setInputEmpty")); exit(EXIT_INVALID_ARGS); } - let Some(resource) = get_resource(dsc, resource_type) else { - error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); + let Some(resource) = get_resource(dsc, resource_type, version) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -161,14 +161,14 @@ pub fn set(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&O } } -pub fn test(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) { +pub fn test(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&OutputFormat>) { if input.is_empty() { error!("{}", t!("resource_command.testInputEmpty")); exit(EXIT_INVALID_ARGS); } - let Some(resource) = get_resource(dsc, resource_type) else { - error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); + let Some(resource) = get_resource(dsc, resource_type, version) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -197,9 +197,9 @@ pub fn test(dsc: &DscManager, resource_type: &str, input: &str, format: Option<& } } -pub fn delete(dsc: &DscManager, resource_type: &str, input: &str) { - let Some(resource) = get_resource(dsc, resource_type) else { - error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); +pub fn delete(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str) { + let Some(resource) = get_resource(dsc, resource_type, version) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -218,9 +218,9 @@ pub fn delete(dsc: &DscManager, resource_type: &str, input: &str) { } } -pub fn schema(dsc: &DscManager, resource_type: &str, format: Option<&OutputFormat>) { - let Some(resource) = get_resource(dsc, resource_type) else { - error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); +pub fn schema(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, format: Option<&OutputFormat>) { + let Some(resource) = get_resource(dsc, resource_type, version) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; if resource.kind == Kind::Adapter { @@ -247,9 +247,9 @@ pub fn schema(dsc: &DscManager, resource_type: &str, format: Option<&OutputForma } } -pub fn export(dsc: &mut DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) { - let Some(dsc_resource) = get_resource(dsc, resource_type) else { - error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); +pub fn export(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&OutputFormat>) { + let Some(dsc_resource) = get_resource(dsc, resource_type, version) else { + error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -275,7 +275,7 @@ pub fn export(dsc: &mut DscManager, resource_type: &str, input: &str, format: Op } #[must_use] -pub fn get_resource<'a>(dsc: &'a DscManager, resource: &str) -> Option<&'a DscResource> { +pub fn get_resource<'a>(dsc: &'a mut DscManager, resource: &str, version: Option<&str>) -> Option<&'a DscResource> { //TODO: add dynamically generated resource to dsc - dsc.find_resource(resource) + dsc.find_resource(resource, version) } diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 1c308ad0..6ce7ec83 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -17,7 +17,7 @@ use dsc_lib::{ config_result::ResourceGetResult, Configurator, }, - discovery::discovery_trait::DiscoveryKind, + discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind}, discovery::command_discovery::ImportedManifest, dscerror::DscError, DscManager, @@ -35,6 +35,7 @@ use dsc_lib::{ }; use regex::RegexBuilder; use rust_i18n::t; +use core::convert::AsRef; use std::{ collections::HashMap, io::{self, IsTerminal}, @@ -488,17 +489,12 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat) }; // discover the resources - let mut resource_types = Vec::new(); + let mut resource_types = Vec::::new(); for resource_block in resources { let Some(type_name) = resource_block["type"].as_str() else { return Err(DscError::Validation(t!("subcommand.resourceTypeNotSpecified").to_string())); }; - - if resource_types.contains(&type_name.to_lowercase()) { - continue; - } - - resource_types.push(type_name.to_lowercase().to_string()); + resource_types.push(DiscoveryFilter::new(&type_name.to_lowercase(), resource_block["api_version"].as_str().map(std::string::ToString::to_string))); } dsc.find_resources(&resource_types, progress_format); @@ -510,7 +506,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat) trace!("{} '{}'", t!("subcommand.validatingResource"), resource_block["name"].as_str().unwrap_or_default()); // get the actual resource - let Some(resource) = get_resource(&dsc, type_name) else { + let Some(resource) = get_resource(&mut dsc, type_name, resource_block["api_version"].as_str()) else { return Err(DscError::Validation(format!("{}: '{type_name}'", t!("subcommand.resourceNotFound")))); }; @@ -577,19 +573,19 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat ResourceSubCommand::List { resource_name, adapter_name, description, tags, output_format } => { list_resources(&mut dsc, resource_name.as_ref(), adapter_name.as_ref(), description.as_ref(), tags.as_ref(), output_format.as_ref(), progress_format); }, - ResourceSubCommand::Schema { resource , output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); - resource_command::schema(&dsc, resource, output_format.as_ref()); + ResourceSubCommand::Schema { resource , version, output_format } => { + dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); + resource_command::schema(&mut dsc, resource, version.as_deref(), output_format.as_ref()); }, - ResourceSubCommand::Export { resource, input, file, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + ResourceSubCommand::Export { resource, version, input, file, output_format } => { + dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); let parsed_input = get_input(input.as_ref(), file.as_ref(), false); - resource_command::export(&mut dsc, resource, &parsed_input, output_format.as_ref()); + resource_command::export(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, - ResourceSubCommand::Get { resource, input, file: path, all, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + ResourceSubCommand::Get { resource, version, input, file: path, all, output_format } => { + dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); if *all { - resource_command::get_all(&dsc, resource, output_format.as_ref()); + resource_command::get_all(&mut dsc, resource, version.as_deref(), output_format.as_ref()); } else { if *output_format == Some(GetOutputFormat::JsonArray) { @@ -597,23 +593,23 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat exit(EXIT_INVALID_ARGS); } let parsed_input = get_input(input.as_ref(), path.as_ref(), false); - resource_command::get(&dsc, resource, &parsed_input, output_format.as_ref()); + resource_command::get(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); } }, - ResourceSubCommand::Set { resource, input, file: path, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + ResourceSubCommand::Set { resource, version, input, file: path, output_format } => { + dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref(), false); - resource_command::set(&dsc, resource, &parsed_input, output_format.as_ref()); + resource_command::set(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, - ResourceSubCommand::Test { resource, input, file: path, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + ResourceSubCommand::Test { resource, version, input, file: path, output_format } => { + dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref(), false); - resource_command::test(&dsc, resource, &parsed_input, output_format.as_ref()); + resource_command::test(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, - ResourceSubCommand::Delete { resource, input, file: path } => { - dsc.find_resources(&[resource.to_string()], progress_format); + ResourceSubCommand::Delete { resource, version, input, file: path } => { + dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref(), false); - resource_command::delete(&dsc, resource, &parsed_input); + resource_command::delete(&mut dsc, resource, version.as_deref(), &parsed_input); }, } } diff --git a/dsc/tests/dsc_config_version.tests.ps1 b/dsc/tests/dsc_config_version.tests.ps1 new file mode 100644 index 00000000..68bc9999 --- /dev/null +++ b/dsc/tests/dsc_config_version.tests.ps1 @@ -0,0 +1,80 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Tests for resource versioning' { + It "Should return the correct version '' for operation ''" -TestCases @( + @{ version = '1.1.2'; operation = 'get'; property = 'actualState' } + @{ version = '1.1.0'; operation = 'get'; property = 'actualState' } + @{ version = '2.0.0'; operation = 'get'; property = 'actualState' } + @{ version = '1.1.2'; operation = 'set'; property = 'afterState' } + @{ version = '1.1.0'; operation = 'set'; property = 'afterState' } + @{ version = '2.0.0'; operation = 'set'; property = 'afterState' } + @{ version = '1.1.2'; operation = 'test'; property = 'actualState' } + @{ version = '1.1.0'; operation = 'test'; property = 'actualState' } + @{ version = '2.0.0'; operation = 'test'; property = 'actualState' } + ) { + param($version, $operation, $property) + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test Version + type: Test/Version + apiVersion: $version + properties: + version: $version +"@ + $out = dsc -l trace config $operation -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[0].result.$property.version | Should -BeExactly $version + } + + It "Version requirements '' should return correct version" -TestCases @( + @{ req = '>=1.0.0' ; expected = '2.0.0' } + @{ req = '<=1.1.0' ; expected = '1.1.0' } + @{ req = '<1.3' ; expected = '1.1.3' } + @{ req = '>1,<=2.0.0' ; expected = '2.0.0' } + @{ req = '>1.0.0,<2.0.0' ; expected = '1.1.3' } + @{ req = '1'; expected = '1.1.3' } + @{ req = '1.1' ; expected = '1.1.3' } + @{ req = '^1.0' ; expected = '1.1.3' } + @{ req = '~1.1' ; expected = '1.1.3' } + @{ req = '*' ; expected = '2.0.0' } + @{ req = '1.*' ; expected = '1.1.3' } + @{ req = '2.1.0-preview.2' ; expected = '2.1.0-preview.2' } + ) { + param($req, $expected) + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test Version + type: Test/Version + apiVersion: '$req' + properties: + version: $expected +"@ + $out = dsc -l trace config test -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[0].result.actualState.version | Should -BeExactly $expected + } + + It 'Multiple versions should be handled correctly' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test Version 1 + type: Test/Version + apiVersion: '1.1.2' + - name: Test Version 2 + type: Test/Version + apiVersion: '1.1.0' + - name: Test Version 3 + type: Test/Version + apiVersion: '2' +"@ + $out = dsc -l trace config get -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $out.results[0].result.actualState.version | Should -BeExactly '1.1.2' + $out.results[1].result.actualState.version | Should -BeExactly '1.1.0' + $out.results[2].result.actualState.version | Should -BeExactly '2.0.0' + } +} diff --git a/dsc/tests/dsc_resource_get.tests.ps1 b/dsc/tests/dsc_resource_get.tests.ps1 index bbac338d..d39be27d 100644 --- a/dsc/tests/dsc_resource_get.tests.ps1 +++ b/dsc/tests/dsc_resource_get.tests.ps1 @@ -70,4 +70,10 @@ Describe 'resource get tests' { $out.bitness | Should -BeIn @('32', '64') $out.architecture | Should -BeIn @('x86', 'x86_64', 'arm64') } + + It 'version works' { + $out = dsc resource get -r Test/Version --version 1.1.2 | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.actualState.version | Should -BeExactly '1.1.2' + } } diff --git a/dsc/tests/dsc_resource_set.tests.ps1 b/dsc/tests/dsc_resource_set.tests.ps1 new file mode 100644 index 00000000..ea7761cf --- /dev/null +++ b/dsc/tests/dsc_resource_set.tests.ps1 @@ -0,0 +1,17 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Invoke a resource set directly' { + It 'set returns proper error code if no input is provided' { + $out = dsc resource set -r Test/Version 2>&1 + $LASTEXITCODE | Should -Be 1 + $out | Should -BeLike '*ERROR*' + } + + It 'version works' { + $out = dsc resource set -r Test/Version --version 1.1.2 --input '{"version":"1.1.2"}' | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.afterState.version | Should -BeExactly '1.1.2' + $out.changedProperties | Should -BeNullOrEmpty + } +} \ No newline at end of file diff --git a/dsc/tests/dsc_resource_test.tests.ps1 b/dsc/tests/dsc_resource_test.tests.ps1 index ad898e54..f9c9d591 100644 --- a/dsc/tests/dsc_resource_test.tests.ps1 +++ b/dsc/tests/dsc_resource_test.tests.ps1 @@ -26,4 +26,11 @@ Describe 'Invoke a resource test directly' { $LASTEXITCODE | Should -Be 1 $out | Should -BeLike '*ERROR*' } + + It 'version works' { + $out = dsc resource test -r Test/Version --version 1.1.2 --input '{"version":"1.1.2"}' | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.actualState.version | Should -BeExactly '1.1.2' + $out.inDesiredState | Should -Be $true + } } \ No newline at end of file diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index b83232c6..07334b20 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -86,9 +86,9 @@ invalidAdapterFilter = "Could not build Regex filter for adapter name" progressSearching = "Searching for resources" extensionSearching = "Searching for extensions" foundResourceManifest = "Found resource manifest: %{path}" -extensionFound = "Extension '%{extension}' found" -adapterFound = "Resource adapter '%{adapter}' found" -resourceFound = "Resource '%{resource}' found" +extensionFound = "Extension '%{extension}' version %{version} found" +adapterFound = "Resource adapter '%{adapter}' version %{version} found" +resourceFound = "Resource '%{resource}' version %{version} found" executableNotFound = "Executable '%{executable}' not found for operation '%{operation}' for resource '%{resource}'" extensionInvalidVersion = "Extension '%{extension}' version '%{version}' is invalid" invalidManifest = "Invalid manifest for resource '%{resource}'" @@ -97,6 +97,11 @@ callingExtension = "Calling extension '%{extension}' to discover resources" extensionFoundResources = "Extension '%{extension}' found %{count} resources" invalidManifestVersion = "Manifest '%{path}' has invalid version: %{err}" importExtensionsEmpty = "Import extension '%{extension}' has no import extensions defined" +searchingForResources = "Searching for resources: %{resources}" +foundResourceWithVersion = "Found matching resource '%{resource}' version %{version}" +invalidRequiredVersion = "Invalid required version '%{version}' for resource '%{resource}'" +foundNonAdapterResources = "Found %{count} non-adapter resources" +foundAdaptedResourceWithVersion = "Found adapted resource '%{resource}' with version %{version}" [dscresources.commandResource] invokeGet = "Invoking get for '%{resource}'" @@ -522,6 +527,7 @@ validSchemaUrisAre = "Valid schema URIs are" extension = "Extension" unsupportedCapability = "does not support capability" setting = "Setting" +invalidRequiredVersion = "Invalid required version" [progress] failedToSerialize = "Failed to serialize progress JSON: %{json}" diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 2ea60d00..52f7bfda 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -3,6 +3,7 @@ use crate::configure::config_doc::{ExecutionKind, Metadata, Resource}; use crate::configure::{config_doc::RestartRequired, parameters::Input}; +use crate::discovery::discovery_trait::DiscoveryFilter; use crate::dscerror::DscError; use crate::dscresources::invoke_result::ExportResult; use crate::dscresources::{ @@ -317,7 +318,7 @@ impl Configurator { let mut result = ConfigurationGetResult::new(); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &self.context)?; let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; - let discovery = &self.discovery.clone(); + let discovery = &mut self.discovery.clone(); for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Get '{}'", resource.name).as_str()); @@ -325,13 +326,11 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { - return Err(DscError::ResourceNotFound(resource.resource_type)); + let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; - debug!("resource_type {}", &resource.resource_type); let filter = add_metadata(&dsc_resource.kind, properties)?; - trace!("filter: {filter}"); let start_datetime = chrono::Local::now(); let mut get_result = match dsc_resource.get(&filter) { Ok(result) => result, @@ -397,7 +396,7 @@ impl Configurator { let mut result = ConfigurationSetResult::new(); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &self.context)?; let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; - let discovery = &self.discovery.clone(); + let discovery = &mut self.discovery.clone(); for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Set '{}'", resource.name).as_str()); @@ -405,8 +404,8 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { - return Err(DscError::ResourceNotFound(resource.resource_type)); + let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); @@ -549,7 +548,7 @@ impl Configurator { let mut result = ConfigurationTestResult::new(); let resources = get_resource_invocation_order(&self.config, &mut self.statement_parser, &self.context)?; let mut progress = ProgressBar::new(resources.len() as u64, self.progress_format)?; - let discovery = &self.discovery.clone(); + let discovery = &mut self.discovery.clone(); for resource in resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Test '{}'", resource.name).as_str()); @@ -557,8 +556,8 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { - return Err(DscError::ResourceNotFound(resource.resource_type)); + let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + return Err(DscError::ResourceNotFound(resource.resource_type, resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(&resource, &dsc_resource.kind)?; debug!("resource_type {}", &resource.resource_type); @@ -626,7 +625,7 @@ impl Configurator { let mut progress = ProgressBar::new(self.config.resources.len() as u64, self.progress_format)?; let resources = self.config.resources.clone(); - let discovery = &self.discovery.clone(); + let discovery = &mut self.discovery.clone(); for resource in &resources { progress.set_resource(&resource.name, &resource.resource_type); progress.write_activity(format!("Export '{}'", resource.name).as_str()); @@ -634,8 +633,8 @@ impl Configurator { progress.write_increment(1); continue; } - let Some(dsc_resource) = discovery.find_resource(&resource.resource_type) else { - return Err(DscError::ResourceNotFound(resource.resource_type.clone())); + let Some(dsc_resource) = discovery.find_resource(&resource.resource_type, resource.api_version.as_deref()) else { + return Err(DscError::ResourceNotFound(resource.resource_type.clone(), resource.api_version.as_deref().unwrap_or("").to_string())); }; let properties = self.get_properties(resource, &dsc_resource.kind)?; let input = add_metadata(&dsc_resource.kind, properties)?; @@ -853,8 +852,16 @@ impl Configurator { check_security_context(config.metadata.as_ref())?; // Perform discovery of resources used in config - let required_resources = config.resources.iter().map(|p| p.resource_type.clone()).collect::>(); - self.discovery.find_resources(&required_resources, self.progress_format); + // create an array of DiscoveryFilter using the resource types and api_versions from the config + let mut discovery_filter: Vec = Vec::new(); + for resource in &config.resources { + let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.clone()); + if !discovery_filter.contains(&filter) { + discovery_filter.push(filter); + } + } + + self.discovery.find_resources(&discovery_filter, self.progress_format); self.config = config; Ok(()) } diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index 0817957b..39df9c3d 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::discovery::discovery_trait::{ResourceDiscovery, DiscoveryKind}; +use crate::discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}; 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; @@ -10,10 +10,9 @@ use crate::extensions::dscextension::{self, DscExtension, Capability as Extensio use crate::extensions::extension_manifest::ExtensionManifest; use crate::progress::{ProgressBar, ProgressFormat}; use crate::util::convert_wildcard_to_regex; -use linked_hash_map::LinkedHashMap; use regex::RegexBuilder; use rust_i18n::t; -use semver::Version; +use semver::{Version, VersionReq}; use serde::Deserialize; use std::collections::{BTreeMap, HashSet, HashMap}; use std::env; @@ -263,7 +262,7 @@ impl ResourceDiscovery for CommandDiscovery { match resource { ImportedManifest::Extension(extension) => { if regex.is_match(&extension.type_name) { - trace!("{}", t!("discovery.commandDiscovery.extensionFound", extension = extension.type_name)); + trace!("{}", t!("discovery.commandDiscovery.extensionFound", extension = extension.type_name, version = extension.version)); // we only keep newest version of the extension so compare the version and only keep the newest if let Some(existing_extension) = extensions.get_mut(&extension.type_name) { let Ok(existing_version) = Version::parse(&existing_extension.version) else { @@ -285,12 +284,12 @@ impl ResourceDiscovery for CommandDiscovery { if let Some(ref manifest) = resource.manifest { let manifest = import_manifest(manifest.clone())?; if manifest.kind == Some(Kind::Adapter) { - trace!("{}", t!("discovery.commandDiscovery.adapterFound", adapter = resource.type_name)); - insert_resource(&mut adapters, &resource, true); + trace!("{}", t!("discovery.commandDiscovery.adapterFound", adapter = resource.type_name, version = resource.version)); + insert_resource(&mut adapters, &resource); } // also make sure to add adapters as a resource as well - trace!("{}", t!("discovery.commandDiscovery.resourceFound", resource = resource.type_name)); - insert_resource(&mut resources, &resource, true); + trace!("{}", t!("discovery.commandDiscovery.resourceFound", resource = resource.type_name, version = resource.version)); + insert_resource(&mut resources, &resource); } } } @@ -315,7 +314,7 @@ impl ResourceDiscovery for CommandDiscovery { for resource in discovered_resources { if regex.is_match(&resource.type_name) { trace!("{}", t!("discovery.commandDiscovery.extensionResourceFound", resource = resource.type_name)); - insert_resource(&mut resources, &resource, true); + insert_resource(&mut resources, &resource); } } } @@ -412,9 +411,7 @@ impl ResourceDiscovery for CommandDiscovery { } if name_regex.is_match(&resource.type_name) { - // we allow duplicate versions since it can come from different adapters - // like PowerShell vs WindowsPowerShell - insert_resource(&mut adapted_resources, &resource, false); + insert_resource(&mut adapted_resources, &resource); adapter_resources_count += 1; } }, @@ -439,10 +436,7 @@ impl ResourceDiscovery for CommandDiscovery { } fn list_available(&mut self, kind: &DiscoveryKind, type_name_filter: &str, adapter_name_filter: &str) -> Result>, DscError> { - - trace!("Listing resources with type_name_filter '{type_name_filter}' and adapter_name_filter '{adapter_name_filter}'"); let mut resources = BTreeMap::>::new(); - if *kind == DiscoveryKind::Resource { if adapter_name_filter.is_empty() { self.discover(kind, type_name_filter)?; @@ -473,84 +467,95 @@ impl ResourceDiscovery for CommandDiscovery { Ok(resources) } - // TODO: handle version requirements - fn find_resources(&mut self, required_resource_types: &[String]) -> Result, DscError> - { - debug!("Searching for resources: {:?}", required_resource_types); + fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter]) -> Result>, DscError> { + debug!("{}", t!("discovery.commandDiscovery.searchingForResources", resources = required_resource_types : {:?})); self.discover( &DiscoveryKind::Resource, "*")?; + let mut found_resources = BTreeMap::>::new(); + let mut required_resources = HashMap::::new(); + for filter in required_resource_types { + required_resources.insert(filter.clone(), false); + } - // convert required_resource_types to lowercase to handle case-insentiive search - let mut remaining_required_resource_types = required_resource_types.iter().map(|x| x.to_lowercase()).collect::>(); - remaining_required_resource_types.sort_unstable(); - remaining_required_resource_types.dedup(); - - let mut found_resources = BTreeMap::::new(); - - for (resource_name, resources) in &self.resources { - // TODO: handle version requirements - let Some(resource ) = resources.first() else { - // skip if no resources - continue; - }; - - if remaining_required_resource_types.contains(&resource_name.to_lowercase()) - { - // remove the resource from the list of required resources - remaining_required_resource_types.retain(|x| *x != resource_name.to_lowercase()); - found_resources.insert(resource_name.to_lowercase(), resource.clone()); - if remaining_required_resource_types.is_empty() - { - return Ok(found_resources); + for filter in required_resource_types { + if let Some(resources) = self.resources.get(filter.resource_type()) { + for resource in resources { + if let Some(required_version) = filter.version() { + if let Ok(resource_version) = Version::parse(&resource.version) { + if let Ok(version_req) = VersionReq::parse(required_version) { + if version_req.matches(&resource_version) { + found_resources.entry(filter.resource_type().to_string()).or_default().push(resource.clone()); + required_resources.insert(filter.clone(), true); + debug!("{}", t!("discovery.commandDiscovery.foundResourceWithVersion", resource = resource.type_name, version = resource.version)); + break; + } + } else { + return Err(DscError::InvalidRequiredVersion(filter.resource_type().to_string(), required_version.to_string())); + } + } else { + warn!("{}", t!("discovery.commandDiscovery.invalidVersionForResource", version = resource.version, resource = resource.type_name)); + } + } else { + // if no version specified, get first one which will be latest + if let Some(resource) = resources.first() { + required_resources.insert(filter.clone(), true); + found_resources.entry(filter.resource_type().to_string()).or_default().push(resource.clone()); + break; + } + } } + } else { + let version = match &filter.version() { + Some(v) => (*v).to_string(), + None => String::new(), + }; + return Err(DscError::ResourceNotFound(filter.resource_type().to_string(), version)); + } + if required_resources.values().all(|&v| v) { + return Ok(found_resources); } } - debug!("Found {} matching non-adapter-based resources", found_resources.len()); - - // now go through the adapters - let sorted_adapters = sort_adapters_based_on_lookup_table(&self.adapters, &remaining_required_resource_types); - for (adapter_name, adapters) in sorted_adapters { - // TODO: handle version requirements - let Some(adapter) = adapters.first() else { - // skip if no adapters - continue; - }; + debug!("{}", t!("discovery.commandDiscovery.foundNonAdapterResources", count = found_resources.len())); - if remaining_required_resource_types.contains(&adapter_name.to_lowercase()) - { - // remove the adapter from the list of required resources - remaining_required_resource_types.retain(|x| *x != adapter_name.to_lowercase()); - found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); - if remaining_required_resource_types.is_empty() - { - return Ok(found_resources); - } - } + if required_resources.values().all(|&v| v) { + return Ok(found_resources); + } - self.discover_adapted_resources("*", &adapter_name)?; - // add/update found adapted resources to the lookup_table + // now go through the adapters, this is for implicit adapters so version can't be specified so use latest version + for adapter_name in self.adapters.clone().keys() { + self.discover_adapted_resources("*", adapter_name)?; add_resources_to_lookup_table(&self.adapted_resources); - - // now go through the adapter resources and add them to the list of resources - for (adapted_name, adapted_resource) in &self.adapted_resources { - let Some(adapted_resource) = adapted_resource.first() else { - // skip if no resources - continue; - }; - - if remaining_required_resource_types.contains(&adapted_name.to_lowercase()) - { - remaining_required_resource_types.retain(|x| *x != adapted_name.to_lowercase()); - found_resources.insert(adapted_name.to_lowercase(), adapted_resource.clone()); - - // also insert the adapter - found_resources.insert(adapter_name.to_lowercase(), adapter.clone()); - if remaining_required_resource_types.is_empty() - { - return Ok(found_resources); + for filter in required_resource_types { + if let Some(adapted_resources) = self.adapted_resources.get(filter.resource_type()) { + for resource in adapted_resources.iter().rev() { + if let Some(required_version) = filter.version() { + if let Ok(resource_version) = Version::parse(&resource.version) { + if let Ok(version_req) = VersionReq::parse(required_version) { + if version_req.matches(&resource_version) { + found_resources.entry(filter.resource_type().to_string()).or_default().push(resource.clone()); + required_resources.insert(filter.clone(), true); + debug!("{}", t!("discovery.commandDiscovery.foundAdaptedResourceWithVersion", resource = resource.type_name, version = resource.version)); + } + } + } else { + warn!("{}", t!("discovery.commandDiscovery.invalidVersionForResource", version = required_version, resource = filter.resource_type())); + } + } else { + // no version specified, use latest + if let Some(resource) = adapted_resources.first() { + found_resources.entry(filter.resource_type().to_string()).or_default().push(resource.clone()); + } + } + if required_resources.values().all(|&v| v) { + break; + } } } } + if required_resources.values().all(|&v| v) { + break; + } } + Ok(found_resources) } @@ -562,10 +567,8 @@ impl ResourceDiscovery for CommandDiscovery { } } -// TODO: This should be a BTreeMap of the resource name and a BTreeMap of the version and DscResource, this keeps it version sorted more efficiently -fn insert_resource(resources: &mut BTreeMap>, resource: &DscResource, skip_duplicate_version: bool) { - if let Some(resource_versions) = resources.get_mut(&resource.type_name) { - debug!("Resource '{}' already exists, checking versions", resource.type_name); +fn insert_resource(resources: &mut BTreeMap>, resource: &DscResource) { + if let Some(resource_versions) = resources.get_mut(&resource.type_name.to_lowercase()) { // compare the resource versions and insert newest to oldest using semver let mut insert_index = resource_versions.len(); for (index, resource_instance) in resource_versions.iter().enumerate() { @@ -581,10 +584,6 @@ fn insert_resource(resources: &mut BTreeMap>, resource: continue; }, }; - // if the version already exists, we might skip it - if skip_duplicate_version && resource_instance_version == resource_version { - return; - } if resource_instance_version < resource_version { insert_index = index; @@ -593,7 +592,7 @@ fn insert_resource(resources: &mut BTreeMap>, resource: } resource_versions.insert(insert_index, resource.clone()); } else { - resources.insert(resource.type_name.clone(), vec![resource.clone()]); + resources.insert(resource.type_name.to_lowercase(), vec![resource.clone()]); } } @@ -754,30 +753,6 @@ fn verify_executable(resource: &str, operation: &str, executable: &str) { } } -fn sort_adapters_based_on_lookup_table(unsorted_adapters: &BTreeMap>, needed_resource_types: &Vec) -> LinkedHashMap> -{ - let mut result = LinkedHashMap::>::new(); - let lookup_table = load_adapted_resources_lookup_table(); - // first add adapters (for needed types) that can be found in the lookup table - for needed_resource in needed_resource_types { - if let Some(adapter_name) = lookup_table.get(needed_resource) { - if let Some(resource_vec) = unsorted_adapters.get(adapter_name) { - debug!("Lookup table found resource '{}' in adapter '{}'", needed_resource, adapter_name); - result.insert(adapter_name.to_string(), resource_vec.clone()); - } - } - } - - // now add remaining adapters - for (adapter_name, adapters) in unsorted_adapters { - if !result.contains_key(adapter_name) { - result.insert(adapter_name.to_string(), adapters.clone()); - } - } - - result -} - fn add_resources_to_lookup_table(adapted_resources: &BTreeMap>) { let mut lookup_table = load_adapted_resources_lookup_table(); diff --git a/dsc_lib/src/discovery/discovery_trait.rs b/dsc_lib/src/discovery/discovery_trait.rs index 2ec7714a..0afea788 100644 --- a/dsc_lib/src/discovery/discovery_trait.rs +++ b/dsc_lib/src/discovery/discovery_trait.rs @@ -3,7 +3,6 @@ use crate::{dscerror::DscError, extensions::dscextension::DscExtension, dscresources::dscresource::DscResource}; use std::collections::BTreeMap; - use super::command_discovery::ImportedManifest; #[derive(Debug, PartialEq)] @@ -12,6 +11,38 @@ pub enum DiscoveryKind { Extension, } +#[derive(Debug, Clone, Eq, Hash, PartialEq)] +pub struct DiscoveryFilter { + resource_type: String, + version: Option, +} + +impl DiscoveryFilter { + #[must_use] + pub fn new(resource_type: &str, version: Option) -> Self { + // The semver crate uses caret (meaning compatible) by default instead of exact if not specified + // If the first character is a number, then we prefix with = + let version = match version { + Some(v) if v.chars().next().is_some_and(|c| c.is_ascii_digit()) => Some(format!("={v}")), + other => other, + }; + Self { + resource_type: resource_type.to_lowercase(), + version, + } + } + + #[must_use] + pub fn resource_type(&self) -> &str { + &self.resource_type + } + + #[must_use] + pub fn version(&self) -> Option<&String> { + self.version.as_ref() + } +} + pub trait ResourceDiscovery { /// Discovery method to find resources. /// @@ -76,7 +107,7 @@ pub trait ResourceDiscovery { /// # Errors /// /// This function will return an error if the underlying discovery fails. - fn find_resources(&mut self, required_resource_types: &[String]) -> Result, DscError>; + fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter]) -> Result>, DscError>; /// Get the available extensions. /// diff --git a/dsc_lib/src/discovery/mod.rs b/dsc_lib/src/discovery/mod.rs index c707c7bc..82a128a2 100644 --- a/dsc_lib/src/discovery/mod.rs +++ b/dsc_lib/src/discovery/mod.rs @@ -4,17 +4,18 @@ pub mod command_discovery; pub mod discovery_trait; -use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery}; +use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery, DiscoveryFilter}; use crate::extensions::dscextension::{Capability, DscExtension}; use crate::{dscresources::dscresource::DscResource, progress::ProgressFormat}; use core::result::Result::Ok; +use semver::{Version, VersionReq}; use std::collections::BTreeMap; use command_discovery::{CommandDiscovery, ImportedManifest}; use tracing::error; #[derive(Clone)] pub struct Discovery { - pub resources: BTreeMap, + pub resources: BTreeMap>, pub extensions: BTreeMap, } @@ -86,8 +87,40 @@ impl Discovery { } #[must_use] - pub fn find_resource(&self, type_name: &str) -> Option<&DscResource> { - self.resources.get(&type_name.to_lowercase()) + pub fn find_resource(&mut self, type_name: &str, version_string: Option<&str>) -> Option<&DscResource> { + if self.resources.is_empty() { + let discovery_filter = DiscoveryFilter::new(type_name, version_string.map(std::string::ToString::to_string)); + self.find_resources(&[discovery_filter], ProgressFormat::None); + } + + let type_name = type_name.to_lowercase(); + if let Some(resources) = self.resources.get(&type_name) { + if let Some(version) = version_string { + // The semver crate uses caret (meaning compatible) by default instead of exact if not specified + // If the first character is a number, then we prefix with = + let version = if version.chars().next().is_some_and(|c| c.is_ascii_digit()) { + format!("={version}") + } else { + version.to_string() + }; + if let Ok(version_req) = VersionReq::parse(&version) { + for resource in resources { + if let Ok(resource_version) = Version::parse(&resource.version) { + if version_req.matches(&resource_version) { + return Some(resource); + } + } + } + None + } else { + None + } + } else { + resources.first() + } + } else { + None + } } /// Find resources based on the required resource types. @@ -95,15 +128,19 @@ impl Discovery { /// # Arguments /// /// * `required_resource_types` - The required resource types. - pub fn find_resources(&mut self, required_resource_types: &[String], progress_format: ProgressFormat) { + pub fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter], progress_format: ProgressFormat) { + if !self.resources.is_empty() { + // If resources are already discovered, no need to re-discover. + return; + } + let command_discovery = CommandDiscovery::new(progress_format); let discovery_types: Vec> = vec![ Box::new(command_discovery), ]; - let mut remaining_required_resource_types = required_resource_types.to_owned(); for mut discovery_type in discovery_types { - let discovered_resources = match discovery_type.find_resources(&remaining_required_resource_types) { + let discovered_resources = match discovery_type.find_resources(required_resource_types) { Ok(value) => value, Err(err) => { error!("{err}"); @@ -111,10 +148,10 @@ impl Discovery { } }; - for resource in discovered_resources { - self.resources.insert(resource.0.clone(), resource.1); - remaining_required_resource_types.retain(|x| *x != resource.0); - }; + for (resource_name, resources) in discovered_resources { + self.resources.entry(resource_name).or_default().extend(resources); + } + if let Ok(extensions) = discovery_type.get_extensions() { self.extensions.extend(extensions); } diff --git a/dsc_lib/src/dscerror.rs b/dsc_lib/src/dscerror.rs index 59b85466..2df37072 100644 --- a/dsc_lib/src/dscerror.rs +++ b/dsc_lib/src/dscerror.rs @@ -50,6 +50,9 @@ pub enum DscError { #[error("{t} '{0}', {t2} {1}, {t3} {2}", t = t!("dscerror.invalidFunctionParameterCount"), t2 = t!("dscerror.expected"), t3 = t!("dscerror.got"))] InvalidFunctionParameterCount(String, usize, usize), + #[error("{t} '{0}': {1}", t = t!("dscerror.invalidRequiredVersion"))] + InvalidRequiredVersion(String, String), + #[error("IO: {0}")] Io(#[from] std::io::Error), @@ -92,8 +95,8 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.progress"))] Progress(#[from] TemplateError), - #[error("{t}: {0}", t = t!("dscerror.resourceNotFound"))] - ResourceNotFound(String), + #[error("{t}: {0} {1}", t = t!("dscerror.resourceNotFound"))] + ResourceNotFound(String, String), #[error("{t}: {0}", t = t!("dscerror.resourceManifestNotFound"))] ResourceManifestNotFound(String), @@ -107,6 +110,9 @@ pub enum DscError { #[error("{t}: {0}", t = t!("dscerror.securityContext"))] SecurityContext(String), + #[error("semver: {0}")] + SemVer(#[from] semver::Error), + #[error("{t}: {0}", t = t!("dscerror.utf8Conversion"))] Utf8Conversion(#[from] Utf8Error), diff --git a/dsc_lib/src/lib.rs b/dsc_lib/src/lib.rs index ee22a025..26b53be7 100644 --- a/dsc_lib/src/lib.rs +++ b/dsc_lib/src/lib.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::discovery::command_discovery::ImportedManifest; +use crate::discovery::{command_discovery::ImportedManifest, discovery_trait::DiscoveryFilter}; use crate::discovery::discovery_trait::DiscoveryKind; use crate::progress::ProgressFormat; @@ -48,15 +48,15 @@ impl DscManager { /// * `name` - The name of the resource to find, can have wildcards. /// #[must_use] - pub fn find_resource(&self, name: &str) -> Option<&DscResource> { - self.discovery.find_resource(name) + pub fn find_resource(&mut self, name: &str, version: Option<&str>) -> Option<&DscResource> { + self.discovery.find_resource(name, version) } pub fn list_available(&mut self, kind: &DiscoveryKind, type_name_filter: &str, adapter_name_filter: &str, progress_format: ProgressFormat) -> Vec { self.discovery.list_available(kind, type_name_filter, adapter_name_filter, progress_format) } - pub fn find_resources(&mut self, required_resource_types: &[String], progress_format: ProgressFormat) { + pub fn find_resources(&mut self, required_resource_types: &[DiscoveryFilter], progress_format: ProgressFormat) { self.discovery.find_resources(required_resource_types, progress_format); } /// Invoke the get operation on a resource. diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 33dad432..b0a31d9b 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -15,6 +15,7 @@ pub enum Schemas { Metadata, Sleep, Trace, + Version, WhatIf, } @@ -93,6 +94,11 @@ pub enum SubCommand { #[clap(name = "trace", about = "The trace level")] Trace, + #[clap(name = "version", about = "Test multiple versions of same resource")] + Version { + version: String, + }, + #[clap(name = "whatif", about = "Check if it is a whatif operation")] WhatIf { #[clap(name = "whatif", short, long, help = "Run as a whatif executionType instead of actual executionType")] diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 8bcb883c..6228a670 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -12,6 +12,7 @@ mod in_desired_state; mod metadata; mod sleep; mod trace; +mod version; mod whatif; use args::{Args, Schemas, SubCommand}; @@ -28,6 +29,7 @@ use crate::in_desired_state::InDesiredState; use crate::metadata::Metadata; use crate::sleep::Sleep; use crate::trace::Trace; +use crate::version::Version; use crate::whatif::WhatIf; use std::{thread, time::Duration}; @@ -229,6 +231,9 @@ fn main() { Schemas::Trace => { schema_for!(Trace) }, + Schemas::Version => { + schema_for!(Version) + }, Schemas::WhatIf => { schema_for!(WhatIf) }, @@ -257,6 +262,12 @@ fn main() { }; serde_json::to_string(&trace).unwrap() }, + SubCommand::Version { version } => { + let version = Version { + version, + }; + serde_json::to_string(&version).unwrap() + }, SubCommand::WhatIf { what_if } => { let result: WhatIf = if what_if { WhatIf { execution_type: "WhatIf".to_string() } diff --git a/tools/dsctest/src/version.rs b/tools/dsctest/src/version.rs new file mode 100644 index 00000000..257fc3e9 --- /dev/null +++ b/tools/dsctest/src/version.rs @@ -0,0 +1,11 @@ +// 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)] +pub struct Version { + pub version: String, +} diff --git a/tools/dsctest/version1.1.2.dsc.resource.json b/tools/dsctest/version1.1.2.dsc.resource.json new file mode 100644 index 00000000..14d1a769 --- /dev/null +++ b/tools/dsctest/version1.1.2.dsc.resource.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Version", + "version": "1.1.2", + "get": { + "executable": "dsctest", + "args": [ + "version", + "1.1.2" + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "version", + "1.1.2" + ], + "return": "state" + }, + "test": { + "executable": "dsctest", + "args": [ + "version", + "1.1.2" + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "version" + ] + } + } +} diff --git a/tools/dsctest/version1.1.3.dsc.resource.json b/tools/dsctest/version1.1.3.dsc.resource.json new file mode 100644 index 00000000..5ebd11ce --- /dev/null +++ b/tools/dsctest/version1.1.3.dsc.resource.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Version", + "version": "1.1.3", + "get": { + "executable": "dsctest", + "args": [ + "version", + "1.1.3" + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "version", + "1.1.3" + ], + "return": "state" + }, + "test": { + "executable": "dsctest", + "args": [ + "version", + "1.1.3" + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "version" + ] + } + } +} diff --git a/tools/dsctest/version1.1.dsc.resource.json b/tools/dsctest/version1.1.dsc.resource.json new file mode 100644 index 00000000..2ec92220 --- /dev/null +++ b/tools/dsctest/version1.1.dsc.resource.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Version", + "version": "1.1.0", + "get": { + "executable": "dsctest", + "args": [ + "version", + "1.1.0" + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "version", + "1.1.0" + ], + "return": "state" + }, + "test": { + "executable": "dsctest", + "args": [ + "version", + "1.1.0" + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "version" + ] + } + } +} diff --git a/tools/dsctest/version2.1p1.dsc.resource.json b/tools/dsctest/version2.1p1.dsc.resource.json new file mode 100644 index 00000000..d75589a4 --- /dev/null +++ b/tools/dsctest/version2.1p1.dsc.resource.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Version", + "version": "2.1.0-preview.1", + "get": { + "executable": "dsctest", + "args": [ + "version", + "2.1.0-preview.1" + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "version", + "2.1.0-preview.1" + ], + "return": "state" + }, + "test": { + "executable": "dsctest", + "args": [ + "version", + "2.1.0-preview.1" + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "version" + ] + } + } +} diff --git a/tools/dsctest/version2.1p2.dsc.resource.json b/tools/dsctest/version2.1p2.dsc.resource.json new file mode 100644 index 00000000..29c9d380 --- /dev/null +++ b/tools/dsctest/version2.1p2.dsc.resource.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Version", + "version": "2.1.0-preview.2", + "get": { + "executable": "dsctest", + "args": [ + "version", + "2.1.0-preview.2" + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "version", + "2.1.0-preview.2" + ], + "return": "state" + }, + "test": { + "executable": "dsctest", + "args": [ + "version", + "2.1.0-preview.2" + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "version" + ] + } + } +} diff --git a/tools/dsctest/version2.dsc.resource.json b/tools/dsctest/version2.dsc.resource.json new file mode 100644 index 00000000..4cc24eae --- /dev/null +++ b/tools/dsctest/version2.dsc.resource.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Version", + "version": "2.0.0", + "get": { + "executable": "dsctest", + "args": [ + "version", + "2.0.0" + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "version", + "2.0.0" + ], + "return": "state" + }, + "test": { + "executable": "dsctest", + "args": [ + "version", + "2.0.0" + ], + "return": "state" + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "version" + ] + } + } +}