diff --git a/.vscode/launch.json b/.vscode/launch.json index c0fe66904..2cde023f0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,6 +20,11 @@ "type": "lldb", "request": "attach", "pid": "${command:pickMyProcess}", + "expressions": "simple", + "preRunCommands": [ + // !! change this path if you placed the script somewhere else !! + "command script import ~/.vscode/rust_prettifier_for_lldb.py" + ], }, { "name": "(Windows) Attach", diff --git a/dsc/tests/dsc_adapter.tests.ps1 b/dsc/tests/dsc_adapter.tests.ps1 new file mode 100644 index 000000000..c54d423e4 --- /dev/null +++ b/dsc/tests/dsc_adapter.tests.ps1 @@ -0,0 +1,91 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Tests for adapter support' { + Context 'Adapter support single resource' { + It 'Direct resource invocation for: ' -TestCases @( + @{ operation = 'get' }, + @{ operation = 'set' }, + @{ operation = 'test' }, + @{ operation = 'export' } + ){ + param($operation) + + $out = dsc resource $operation -r Adapted/One -i '{"one":"1"}' 2>$TestDrive/error.log | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) + switch ($operation) { + 'get' { + $out.actualState.one | Should -BeExactly 'value1' + } + 'set' { + $out.afterState.one | Should -BeExactly 'value1' + } + 'test' { + $out.actualState.one | Should -BeExactly 'value1' + $out.inDesiredState | Should -BeFalse + $out.differingProperties | Should -Be @('one') + } + 'export' { + $out.resources.count | Should -Be 2 + $out.resources[0].type | Should -BeExactly 'Adapted/One' + $out.resources[0].name | Should -BeExactly 'first' + $out.resources[0].properties.one | Should -BeExactly 'first1' + $out.resources[1].type | Should -BeExactly 'Adapted/One' + $out.resources[1].name | Should -BeExactly 'second' + $out.resources[1].properties.one | Should -BeExactly 'second1' + } + } + } + + It 'Config resource invocation for: ' -TestCases @( + @{ operation = 'get' }, + @{ operation = 'set' }, + @{ operation = 'test' }, + @{ operation = 'export' } + ){ + param($operation) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Test + type: Adapted/Two + properties: + two: '2' +"@ + $out = dsc config $operation -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json -Depth 10 + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log | Out-String) + switch ($operation) { + 'get' { + $out.results.Count | Should -Be 1 + $out.results[0].Name | Should -Be 'Test' + $out.results[0].type | Should -BeExactly 'Adapted/Two' + $out.results[0].result.actualState.two | Should -BeExactly 'value2' -Because ($out | ConvertTo-Json -Depth 10 | Out-String) + } + 'set' { + $out.results.Count | Should -Be 1 + $out.results[0].Name | Should -Be 'Test' + $out.results[0].type | Should -BeExactly 'Adapted/Two' + $out.results[0].result.afterState.two | Should -BeExactly 'value2' -Because ($out | ConvertTo-Json -Depth 10 | Out-String) + } + 'test' { + $out.results.Count | Should -Be 1 + $out.results[0].Name | Should -Be 'Test' + $out.results[0].type | Should -BeExactly 'Adapted/Two' + $out.results[0].result.actualState.two | Should -BeExactly 'value2' -Because ($out | ConvertTo-Json -Depth 10 | Out-String) + $out.results[0].result.inDesiredState | Should -BeFalse + $out.results[0].result.differingProperties | Should -Be @('two') -Because ($out | ConvertTo-Json -Depth 10 | Out-String) + } + 'export' { + $out.resources.Count | Should -Be 2 + $out.resources[0].Name | Should -Be 'first' + $out.resources[0].type | Should -BeExactly 'Adapted/Two' + $out.resources[0].properties.two | Should -BeExactly 'first2' + $out.resources[1].Name | Should -Be 'second' + $out.resources[1].type | Should -BeExactly 'Adapted/Two' + $out.resources[1].properties.two | Should -BeExactly 'second2' + } + } + } + } +} diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 47892e7bb..ecaa1cbd0 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -195,6 +195,9 @@ resourceImplementsValidate = "Resource implements validation" resourceValidationFailed = "Resource failed validation" validatingSchema = "Validating against schema" validationFailed = "Failed validation" +adapterResourceNotFound = "Adapter resource '%{adapter}' not found" +adapterManifestNotFound = "Adapter manifest for '%{adapter}' not found" +adapterDoesNotSupportDelete = "Adapter '%{adapter}' does not support delete operation" [dscresources.resource_manifest] resourceManifestSchemaTitle = "Resource manifest schema URI" diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index a96fd5b45..4d5ecc1b0 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -6,11 +6,10 @@ use crate::configure::context::{Context, ProcessMode}; 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::{ - {dscresource::{Capability, Invoke, get_diff, validate_properties}, - invoke_result::{GetResult, SetResult, TestResult, ResourceSetResponse}}, - resource_manifest::Kind, + {dscresource::{Capability, Invoke, get_diff, validate_properties, get_adapter_input_kind}, + invoke_result::{GetResult, SetResult, TestResult, ExportResult, ResourceSetResponse}}, + resource_manifest::{AdapterInputKind, Kind}, }; use crate::DscResource; use crate::discovery::Discovery; @@ -177,7 +176,7 @@ fn escape_property_values(properties: &Map) -> Result>, resource_metadata: Option ) -> Result { - if dsc_resource.kind == Kind::Adapter { + if dsc_resource.kind == Kind::Adapter && get_adapter_input_kind(dsc_resource)? == AdapterInputKind::Full { // add metadata to the properties so the adapter knows this is a config let mut metadata: Map = Map::new(); if let Some(resource_metadata) = resource_metadata { @@ -319,6 +318,15 @@ impl Configurator { &self.config } + /// Get the discovery. + /// + /// # Returns + /// + /// * `&Discovery` - The discovery. + pub fn discovery(&mut self) -> &mut Discovery { + &mut self.discovery + } + fn get_properties(&mut self, resource: &Resource, resource_kind: &Kind) -> Result>, DscError> { match resource_kind { Kind::Group => { @@ -342,14 +350,14 @@ impl Configurator { /// This function will return an error if the underlying resource fails. pub fn invoke_get(&mut self) -> Result { self.unroll_copy_loops()?; - + 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 = &mut self.discovery.clone(); for resource in resources { let evaluated_name = self.evaluate_resource_name(&resource.name)?; - + progress.set_resource(&evaluated_name, &resource.resource_type); progress.write_activity(format!("Get '{evaluated_name}'").as_str()); if self.skip_resource(&resource)? { @@ -424,14 +432,14 @@ impl Configurator { #[allow(clippy::too_many_lines)] pub fn invoke_set(&mut self, skip_test: bool) -> Result { self.unroll_copy_loops()?; - + 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 = &mut self.discovery.clone(); for resource in resources { let evaluated_name = self.evaluate_resource_name(&resource.name)?; - + progress.set_resource(&evaluated_name, &resource.resource_type); progress.write_activity(format!("Set '{evaluated_name}'").as_str()); if self.skip_resource(&resource)? { @@ -580,14 +588,14 @@ impl Configurator { /// This function will return an error if the underlying resource fails. pub fn invoke_test(&mut self) -> Result { self.unroll_copy_loops()?; - + 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 = &mut self.discovery.clone(); for resource in resources { let evaluated_name = self.evaluate_resource_name(&resource.name)?; - + progress.set_resource(&evaluated_name, &resource.resource_type); progress.write_activity(format!("Test '{evaluated_name}'").as_str()); if self.skip_resource(&resource)? { @@ -658,7 +666,7 @@ impl Configurator { /// This function will return an error if the underlying resource fails. pub fn invoke_export(&mut self) -> Result { self.unroll_copy_loops()?; - + let mut result = ConfigurationExportResult::new(); let mut conf = config_doc::Configuration::new(); conf.metadata.clone_from(&self.config.metadata); @@ -668,7 +676,7 @@ impl Configurator { let discovery = &mut self.discovery.clone(); for resource in &resources { let evaluated_name = self.evaluate_resource_name(&resource.name)?; - + progress.set_resource(&evaluated_name, &resource.resource_type); progress.write_activity(format!("Export '{evaluated_name}'").as_str()); if self.skip_resource(resource)? { @@ -916,7 +924,7 @@ impl Configurator { fn unroll_copy_loops(&mut self) -> Result<(), DscError> { let mut config = self.config.clone(); let config_copy = config.clone(); - + for resource in config_copy.resources { // if the resource contains `Copy`, unroll it if let Some(copy) = &resource.copy { @@ -931,7 +939,7 @@ impl Configurator { return Err(DscError::Parser(t!("configure.mod.copyNameResultNotString").to_string())) }; new_resource.name = new_name.to_string(); - + new_resource.copy = None; copy_resources.push(new_resource); } @@ -941,7 +949,7 @@ impl Configurator { config.resources.extend(copy_resources); } } - + self.config = config; Ok(()) } @@ -967,12 +975,12 @@ impl Configurator { if self.context.process_mode == ProcessMode::Copy { return Ok(name.to_string()); } - + // evaluate the resource name (handles both expressions and literals) let Value::String(evaluated_name) = self.statement_parser.parse_and_execute(name, &self.context)? else { return Err(DscError::Parser(t!("configure.mod.nameResultNotString").to_string())) }; - + Ok(evaluated_name) } diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index dc89e2b34..68bf4484a 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -25,13 +25,17 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; /// # Errors /// /// Error returned if the resource does not successfully get the current state -pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Result { +pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str, target_resource: Option<&str>) -> Result { debug!("{}", t!("dscresources.commandResource.invokeGet", resource = &resource.resource_type)); let mut command_input = CommandInput { env: None, stdin: None }; let Some(get) = &resource.get else { return Err(DscError::NotImplemented("get".to_string())); }; - let args = process_args(get.args.as_ref(), filter); + let resource_type = match target_resource { + Some(r) => r, + None => &resource.resource_type, + }; + let args = process_args(get.args.as_ref(), filter, resource_type); if !filter.is_empty() { verify_json(resource, cwd, filter)?; command_input = get_command_input(get.input.as_ref(), filter)?; @@ -74,7 +78,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul /// /// Error returned if the resource does not successfully set the desired state #[allow(clippy::too_many_lines)] -pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { +pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_test: bool, execution_type: &ExecutionKind, target_resource: Option<&str>) -> Result { debug!("{}", t!("dscresources.commandResource.invokeSet", resource = &resource.resource_type)); let operation_type: String; let mut is_synthetic_what_if = false; @@ -101,7 +105,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te // if resource doesn't implement a pre-test, we execute test first to see if a set is needed if !skip_test && set.pre_test != Some(true) { info!("{}", t!("dscresources.commandResource.noPretest", resource = &resource.resource_type)); - let test_result = invoke_test(resource, cwd, desired)?; + let test_result = invoke_test(resource, cwd, desired, target_resource)?; if is_synthetic_what_if { return Ok(test_result.into()); } @@ -136,7 +140,11 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te let Some(get) = &resource.get else { return Err(DscError::NotImplemented("get".to_string())); }; - let args = process_args(get.args.as_ref(), desired); + let resource_type = match target_resource { + Some(r) => r, + None => &resource.resource_type, + }; + let args = process_args(get.args.as_ref(), desired, resource_type); let command_input = get_command_input(get.input.as_ref(), desired)?; info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.resource_type, executable = &get.executable)); @@ -168,7 +176,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let args = process_args(set.args.as_ref(), desired); + let args = process_args(set.args.as_ref(), desired, resource_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -228,7 +236,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te }, None => { // perform a get and compare the result to the expected state - let get_result = invoke_get(resource, cwd, desired)?; + let get_result = invoke_get(resource, cwd, desired, target_resource)?; // for changed_properties, we compare post state to pre state let actual_state = match get_result { GetResult::Group(results) => { @@ -263,16 +271,20 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Result { +pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str, target_resource: Option<&str>) -> Result { debug!("{}", t!("dscresources.commandResource.invokeTest", resource = &resource.resource_type)); let Some(test) = &resource.test else { info!("{}", t!("dscresources.commandResource.testSyntheticTest", resource = &resource.resource_type)); - return invoke_synthetic_test(resource, cwd, expected); + return invoke_synthetic_test(resource, cwd, expected, target_resource); }; verify_json(resource, cwd, expected)?; - let args = process_args(test.args.as_ref(), expected); + let resource_type = match target_resource { + Some(r) => r, + None => &resource.resource_type, + }; + let args = process_args(test.args.as_ref(), expected, resource_type); let command_input = get_command_input(test.input.as_ref(), expected)?; info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.resource_type, executable = &test.executable)); @@ -330,7 +342,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re }, None => { // perform a get and compare the result to the expected state - let get_result = invoke_get(resource, cwd, expected)?; + let get_result = invoke_get(resource, cwd, expected, target_resource)?; let actual_state = match get_result { GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); @@ -368,8 +380,8 @@ fn get_desired_state(actual: &Value) -> Result, DscError> { Ok(in_desired_state) } -fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Result { - let get_result = invoke_get(resource, cwd, expected)?; +fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &str, expected: &str, target_resource: Option<&str>) -> Result { + let get_result = invoke_get(resource, cwd, expected, target_resource)?; let actual_state = match get_result { GetResult::Group(results) => { let mut result_array: Vec = Vec::new(); @@ -403,17 +415,21 @@ fn invoke_synthetic_test(resource: &ResourceManifest, cwd: &str, expected: &str) /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Result<(), DscError> { +pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str, target_resource: Option<&str>) -> Result<(), DscError> { let Some(delete) = &resource.delete else { return Err(DscError::NotImplemented("delete".to_string())); }; verify_json(resource, cwd, filter)?; - let args = process_args(delete.args.as_ref(), filter); + let resource_type = match target_resource { + Some(r) => r, + None => &resource.resource_type, + }; + let args = process_args(delete.args.as_ref(), filter, resource_type); let command_input = get_command_input(delete.input.as_ref(), filter)?; - info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = &resource.resource_type, executable = &delete.executable)); + info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; Ok(()) @@ -434,17 +450,21 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Re /// # Errors /// /// Error is returned if the underlying command returns a non-zero exit code. -pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) -> Result { +pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str, target_resource: Option<&str>) -> Result { trace!("{}", t!("dscresources.commandResource.invokeValidateConfig", resource = &resource.resource_type, config = &config)); // TODO: use schema to validate config if validate is not implemented let Some(validate) = resource.validate.as_ref() else { return Err(DscError::NotImplemented("validate".to_string())); }; - let args = process_args(validate.args.as_ref(), config); + let resource_type = match target_resource { + Some(r) => r, + None => &resource.resource_type, + }; + let args = process_args(validate.args.as_ref(), config, resource_type); let command_input = get_command_input(validate.input.as_ref(), config)?; - info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = &resource.resource_type, executable = &validate.executable)); + info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; let result: ValidateResult = serde_json::from_str(&stdout)?; Ok(result) @@ -491,12 +511,12 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result) -> Result { +pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str>, target_resource: Option<&str>) -> Result { let Some(export) = resource.export.as_ref() else { // see if get is supported and use that instead if resource.get.is_some() { info!("{}", t!("dscresources.commandResource.exportNotSupportedUsingGet", resource = &resource.resource_type)); - let get_result = invoke_get(resource, cwd, input.unwrap_or(""))?; + let get_result = invoke_get(resource, cwd, input.unwrap_or(""), target_resource)?; let mut instances: Vec = Vec::new(); match get_result { GetResult::Group(group_response) => { @@ -518,6 +538,10 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str> let mut command_input: CommandInput = CommandInput { env: None, stdin: None }; let args: Option>; + let resource_type = match target_resource { + Some(r) => r, + None => &resource.resource_type, + }; if let Some(input) = input { if !input.is_empty() { verify_json(resource, cwd, input)?; @@ -525,9 +549,9 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str> command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_args(export.args.as_ref(), input); + args = process_args(export.args.as_ref(), input, resource_type); } else { - args = process_args(export.args.as_ref(), ""); + args = process_args(export.args.as_ref(), "", resource_type); } let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, resource.exit_codes.as_ref())?; @@ -572,7 +596,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &str, input: &str) -> Re return Err(DscError::Operation(t!("dscresources.commandResource.resolveNotSupported", resource = &resource.resource_type).to_string())); }; - let args = process_args(resolve.args.as_ref(), input); + let args = process_args(resolve.args.as_ref(), input, &resource.resource_type); let command_input = get_command_input(resolve.input.as_ref(), input)?; info!("{}", t!("dscresources.commandResource.invokeResolveUsing", resource = &resource.resource_type, executable = &resolve.executable)); @@ -749,7 +773,7 @@ pub async fn invoke_command(executable: &str, args: Option>, input: /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_args(args: Option<&Vec>, value: &str) -> Option> { +pub fn process_args(args: Option<&Vec>, input: &str, resource_type: &str) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -762,13 +786,17 @@ pub fn process_args(args: Option<&Vec>, value: &str) -> Option { - if value.is_empty() && *mandatory != Some(true) { + if input.is_empty() && *mandatory != Some(true) { continue; } processed_args.push(json_input_arg.clone()); - processed_args.push(value.to_string()); + processed_args.push(input.to_string()); }, + ArgKind::ResourceType { resource_type_arg } => { + processed_args.push(resource_type_arg.clone()); + processed_args.push(resource_type.to_string()); + } } } @@ -811,7 +839,7 @@ fn verify_json(resource: &ResourceManifest, cwd: &str, json: &str) -> Result<(), // see if resource implements validate if resource.validate.is_some() { trace!("{}", t!("dscresources.commandResource.validateJson", json = json)); - let result = invoke_validate(resource, cwd, json)?; + let result = invoke_validate(resource, cwd, json, None)?; if result.valid { return Ok(()); } diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index 0ee2e0796..07d2b470e 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::Kind}; +use crate::{configure::{Configurator, config_doc::{Configuration, ExecutionKind, Resource}, context::ProcessMode, parameters::{SECURE_VALUE_REDACTED, is_secure_value}}, dscresources::resource_manifest::{AdapterInputKind, Kind}}; use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use dscerror::DscError; use jsonschema::Validator; @@ -51,6 +51,8 @@ pub struct DscResource { /// The required resource adapter for the resource. #[serde(rename="requireAdapter")] pub require_adapter: Option, + /// The target resource for the resource adapter. + pub target_resource: Option, /// The manifest of the resource. pub manifest: Option, } @@ -100,6 +102,7 @@ impl DscResource { author: None, properties: Vec::new(), require_adapter: None, + target_resource: None, manifest: None, } } @@ -129,6 +132,149 @@ impl DscResource { configurator.context.process_mode = ProcessMode::NoExpressionEvaluation; Ok(configurator) } + + fn invoke_get_with_adapter(&self, adapter: &str, resource_name: &str, filter: &str) -> Result { + let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; + let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; + if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + adapter.target_resource = Some(resource_name.to_string()); + return adapter.get(filter); + } + + let result = configurator.invoke_get()?; + let GetResult::Resource(ref resource_result) = result.results[0].result else { + return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "get", resource = self.type_name).to_string())); + }; + let properties = resource_result.actual_state + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "actualState", property_type = "object").to_string()))? + .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? + .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? + .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); + let get_result = GetResult::Resource(ResourceGetResponse { + actual_state: properties.clone(), + }); + Ok(get_result) + } + + fn invoke_set_with_adapter(&self, adapter: &str, resource_name: &str, desired: &str, skip_test: bool, execution_type: &ExecutionKind) -> Result { + let mut configurator = self.clone().create_config_for_adapter(adapter, desired)?; + let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; + if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + adapter.target_resource = Some(resource_name.to_string()); + return adapter.set(desired, skip_test, execution_type); + } + + let result = configurator.invoke_set(false)?; + let SetResult::Resource(ref resource_result) = result.results[0].result else { + return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "set", resource = self.type_name).to_string())); + }; + let before_state = resource_result.before_state + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "beforeState", property_type = "object").to_string()))? + .get("resources").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "resources").to_string()))? + .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? + .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); + let after_state = resource_result.after_state + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "afterState", property_type = "object").to_string()))? + .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? + .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? + .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); + let diff = get_diff(&before_state, &after_state); + let set_result = SetResult::Resource(ResourceSetResponse { + before_state: before_state.clone(), + after_state: after_state.clone(), + changed_properties: if diff.is_empty() { None } else { Some(diff) }, + }); + Ok(set_result) + } + + fn invoke_test_with_adapter(&self, adapter: &str, resource_name: &str, expected: &str) -> Result { + let mut configurator = self.clone().create_config_for_adapter(adapter, expected)?; + let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; + if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + adapter.target_resource = Some(resource_name.to_string()); + return adapter.test(expected); + } + + let result = configurator.invoke_test()?; + let TestResult::Resource(ref resource_result) = result.results[0].result else { + return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "test", resource = self.type_name).to_string())); + }; + let desired_state = resource_result.desired_state + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "desiredState", property_type = "object").to_string()))? + .get("resources").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "resources").to_string()))? + .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "resources", property_type = "array").to_string()))?[0] + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "resources", property_type = "object").to_string()))? + .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); + let actual_state = resource_result.actual_state + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "actualState", property_type = "object").to_string()))? + .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? + .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] + .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? + .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); + let diff_properties = get_diff(&desired_state, &actual_state); + let test_result = TestResult::Resource(ResourceTestResponse { + desired_state, + actual_state, + in_desired_state: resource_result.in_desired_state, + diff_properties, + }); + Ok(test_result) + } + + fn invoke_delete_with_adapter(&self, adapter: &str, resource_name: &str, filter: &str) -> Result<(), DscError> { + let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; + let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; + if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + if adapter.capabilities.contains(&Capability::Delete) { + adapter.target_resource = Some(resource_name.to_string()); + return adapter.delete(filter); + } + return Err(DscError::NotSupported(t!("dscresources.dscresource.adapterDoesNotSupportDelete", adapter = adapter.type_name).to_string())); + } + + configurator.invoke_set(false)?; + Ok(()) + } + + fn invoke_export_with_adapter(&self, adapter: &str, input: &str) -> Result { + let mut configurator = self.clone().create_config_for_adapter(adapter, input)?; + let mut adapter = Self::get_adapter_resource(&mut configurator, adapter)?; + if get_adapter_input_kind(&adapter)? == AdapterInputKind::Single { + adapter.target_resource = Some(self.type_name.clone()); + return adapter.export(input); + } + + let result = configurator.invoke_export()?; + let Some(configuration) = result.result else { + return Err(DscError::Operation(t!("dscresources.dscresource.invokeExportReturnedNoResult", resource = self.type_name).to_string())); + }; + let mut export_result = ExportResult { + actual_state: Vec::new(), + }; + debug!("Export result: {}", serde_json::to_string(&configuration)?); + for resource in configuration.resources { + let Some(properties) = resource.properties else { + return Err(DscError::Operation(t!("dscresources.dscresource.invokeExportReturnedNoResult", resource = self.type_name).to_string())); + }; + let results = properties + .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? + .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?; + for result in results { + export_result.actual_state.push(serde_json::to_value(result.clone())?); + } + } + Ok(export_result) + } + + fn get_adapter_resource(configurator: &mut Configurator, adapter: &str) -> Result { + if let Some(adapter_resource) = configurator.discovery().find_resource(adapter, None) { + return Ok(adapter_resource.clone()); + } + Err(DscError::Operation(t!("dscresources.dscresource.adapterResourceNotFound", adapter = adapter).to_string())) + } } impl Default for DscResource { @@ -229,21 +375,7 @@ impl Invoke for DscResource { fn get(&self, filter: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeGet", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; - let result = configurator.invoke_get()?; - let GetResult::Resource(ref resource_result) = result.results[0].result else { - return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "get", resource = self.type_name).to_string())); - }; - let properties = resource_result.actual_state - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "actualState", property_type = "object").to_string()))? - .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? - .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? - .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); - let get_result = GetResult::Resource(ResourceGetResponse { - actual_state: properties.clone(), - }); - return Ok(get_result); + return self.invoke_get_with_adapter(adapter, &self.type_name, filter); } match &self.implemented_as { @@ -255,7 +387,7 @@ impl Invoke for DscResource { return Err(DscError::MissingManifest(self.type_name.clone())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_get(&resource_manifest, &self.directory, filter) + command_resource::invoke_get(&resource_manifest, &self.directory, filter, self.target_resource.as_deref()) }, } } @@ -263,30 +395,7 @@ 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(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, desired)?; - let result = configurator.invoke_set(false)?; - let SetResult::Resource(ref resource_result) = result.results[0].result else { - return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "set", resource = self.type_name).to_string())); - }; - let before_state = resource_result.before_state - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "beforeState", property_type = "object").to_string()))? - .get("resources").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "resources").to_string()))? - .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? - .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); - let after_state = resource_result.after_state - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "afterState", property_type = "object").to_string()))? - .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? - .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? - .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); - let diff = get_diff(&before_state, &after_state); - let set_result = SetResult::Resource(ResourceSetResponse { - before_state: before_state.clone(), - after_state: after_state.clone(), - changed_properties: if diff.is_empty() { None } else { Some(diff) }, - }); - return Ok(set_result); + return self.invoke_set_with_adapter(adapter, &self.type_name, desired, skip_test, execution_type); } match &self.implemented_as { @@ -298,7 +407,7 @@ impl Invoke for DscResource { return Err(DscError::MissingManifest(self.type_name.clone())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type) + command_resource::invoke_set(&resource_manifest, &self.directory, desired, skip_test, execution_type, self.target_resource.as_deref()) }, } } @@ -306,32 +415,7 @@ impl Invoke for DscResource { fn test(&self, expected: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeTest", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, expected)?; - let result = configurator.invoke_test()?; - let TestResult::Resource(ref resource_result) = result.results[0].result else { - return Err(DscError::Operation(t!("dscresources.dscresource.invokeReturnedWrongResult", operation = "test", resource = self.type_name).to_string())); - }; - let mut desired_state = resource_result.desired_state - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "desiredState", property_type = "object").to_string()))? - .get("resources").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "resources").to_string()))? - .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "resources", property_type = "array").to_string()))?[0] - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "resources", property_type = "object").to_string()))? - .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); - let actual_state = resource_result.actual_state - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "actualState", property_type = "object").to_string()))? - .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? - .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?[0] - .as_object().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "object").to_string()))? - .get("properties").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "properties").to_string()))?.clone(); - let diff_properties = get_diff(&desired_state, &actual_state); - desired_state = redact(&desired_state); - let test_result = TestResult::Resource(ResourceTestResponse { - desired_state, - actual_state, - in_desired_state: resource_result.in_desired_state, - diff_properties, - }); - return Ok(test_result); + return self.invoke_test_with_adapter(adapter, &self.type_name, expected); } match &self.implemented_as { @@ -371,7 +455,7 @@ impl Invoke for DscResource { Ok(test_result) } else { - command_resource::invoke_test(&resource_manifest, &self.directory, expected) + command_resource::invoke_test(&resource_manifest, &self.directory, expected, self.target_resource.as_deref()) } }, } @@ -380,9 +464,7 @@ impl Invoke for DscResource { fn delete(&self, filter: &str) -> Result<(), DscError> { debug!("{}", t!("dscresources.dscresource.invokeDelete", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, filter)?; - configurator.invoke_set(false)?; - return Ok(()); + return self.invoke_delete_with_adapter(adapter, &self.type_name, filter); } match &self.implemented_as { @@ -394,7 +476,7 @@ impl Invoke for DscResource { return Err(DscError::MissingManifest(self.type_name.clone())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_delete(&resource_manifest, &self.directory, filter) + command_resource::invoke_delete(&resource_manifest, &self.directory, filter, self.target_resource.as_deref()) }, } } @@ -414,7 +496,7 @@ impl Invoke for DscResource { return Err(DscError::MissingManifest(self.type_name.clone())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_validate(&resource_manifest, &self.directory, config) + command_resource::invoke_validate(&resource_manifest, &self.directory, config, self.target_resource.as_deref()) }, } } @@ -442,34 +524,14 @@ impl Invoke for DscResource { fn export(&self, input: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeExport", resource = self.type_name)); if let Some(adapter) = &self.require_adapter { - let mut configurator = self.clone().create_config_for_adapter(adapter, input)?; - let result = configurator.invoke_export()?; - let Some(configuration) = result.result else { - return Err(DscError::Operation(t!("dscresources.dscresource.invokeExportReturnedNoResult", resource = self.type_name).to_string())); - }; - let mut export_result = ExportResult { - actual_state: Vec::new(), - }; - debug!("Export result: {}", serde_json::to_string(&configuration)?); - for resource in configuration.resources { - let Some(properties) = resource.properties else { - return Err(DscError::Operation(t!("dscresources.dscresource.invokeExportReturnedNoResult", resource = self.type_name).to_string())); - }; - let results = properties - .get("result").ok_or(DscError::Operation(t!("dscresources.dscresource.propertyNotFound", property = "result").to_string()))? - .as_array().ok_or(DscError::Operation(t!("dscresources.dscresource.propertyIncorrectType", property = "result", property_type = "array").to_string()))?; - for result in results { - export_result.actual_state.push(serde_json::to_value(result.clone())?); - } - } - return Ok(export_result); + return self.invoke_export_with_adapter(adapter, input); } let Some(manifest) = &self.manifest else { return Err(DscError::MissingManifest(self.type_name.clone())); }; let resource_manifest = import_manifest(manifest.clone())?; - command_resource::invoke_export(&resource_manifest, &self.directory, Some(input)) + command_resource::invoke_export(&resource_manifest, &self.directory, Some(input), self.target_resource.as_deref()) } fn resolve(&self, input: &str) -> Result { @@ -523,6 +585,27 @@ pub fn redact(value: &Value) -> Value { value.clone() } +/// Gets the input kind for an adapter resource +/// +/// # Arguments +/// * `adapter` - The adapter resource to get the input kind for +/// +/// # Returns +/// * `Result` - The input kind of the adapter or an error if not found +/// +/// # Errors +/// * `DscError` - The adapter manifest is not found or invalid +pub fn get_adapter_input_kind(adapter: &DscResource) -> Result { + if let Some(manifest) = &adapter.manifest { + if let Ok(manifest) = serde_json::from_value::(manifest.clone()) { + if let Some(adapter_operation) = manifest.adapter { + return Ok(adapter_operation.input_kind); + } + } + } + Err(DscError::Operation(t!("dscresources.dscresource.adapterManifestNotFound", adapter = adapter.type_name).to_string())) +} + #[must_use] /// Performs a comparison of two JSON Values if the expected is a strict subset of the actual /// diff --git a/dsc_lib/src/dscresources/resource_manifest.rs b/dsc_lib/src/dscresources/resource_manifest.rs index a5b0f48ff..34d3f070a 100644 --- a/dsc_lib/src/dscresources/resource_manifest.rs +++ b/dsc_lib/src/dscresources/resource_manifest.rs @@ -86,6 +86,11 @@ pub enum ArgKind { /// Indicates if argument is mandatory which will pass an empty string if no JSON input is provided. Default is false. mandatory: Option, }, + ResourceType { + /// The argument that accepts the resource type name. + #[serde(rename = "resourceTypeArg")] + resource_type_arg: String, + } } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] @@ -213,18 +218,22 @@ pub struct ResolveMethod { pub struct Adapter { /// The way to list adapter supported resources. pub list: ListMethod, - /// Defines how the adapter supports accepting configuraiton. - pub config: ConfigKind, + /// Defines how the adapter supports accepting configuration. + #[serde(alias = "config", rename = "inputKind")] + pub input_kind: AdapterInputKind, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -pub enum ConfigKind { +pub enum AdapterInputKind { /// The adapter accepts full unprocessed configuration. #[serde(rename = "full")] Full, /// The adapter accepts configuration as a sequence. #[serde(rename = "sequence")] Sequence, + /// The adapter accepts a single resource input. + #[serde(rename = "single")] + Single, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] diff --git a/dsc_lib/src/extensions/discover.rs b/dsc_lib/src/extensions/discover.rs index 1a716565e..5fd2cef91 100644 --- a/dsc_lib/src/extensions/discover.rs +++ b/dsc_lib/src/extensions/discover.rs @@ -65,7 +65,7 @@ impl DscExtension { let Some(discover) = extension.discover else { return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Discover.to_string())); }; - let args = process_args(discover.args.as_ref(), ""); + let args = process_args(discover.args.as_ref(), "", &self.type_name); let (_exit_code, stdout, _stderr) = invoke_command( &discover.executable, args, diff --git a/tools/dsctest/resourceadapter.dsc.resource.json b/tools/dsctest/resourceadapter.dsc.resource.json new file mode 100644 index 000000000..f6acb9634 --- /dev/null +++ b/tools/dsctest/resourceadapter.dsc.resource.json @@ -0,0 +1,96 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Adapter", + "version": "0.1.0", + "kind": "adapter", + "adapter": { + "list": { + "executable": "dsctest", + "args": [ + "adapter", + "--operation", + "list", + "--input", + "{}", + "--resource-type", + "none" + ] + }, + "inputKind": "single" + }, + "get": { + "executable": "dsctest", + "args": [ + "adapter", + "--operation", + "get", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "resourceTypeArg": "--resource-type" + } + ] + }, + "set": { + "executable": "dsctest", + "args": [ + "adapter", + "--operation", + "set", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "resourceTypeArg": "--resource-type" + } + ] + }, + "test": { + "executable": "dsctest", + "args": [ + "adapter", + "--operation", + "test", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "resourceTypeArg": "--resource-type" + } + ] + }, + "export": { + "executable": "dsctest", + "args": [ + "adapter", + "--operation", + "export", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "resourceTypeArg": "--resource-type" + } + ] + }, + "validate": { + "executable": "dsctest", + "args": [ + "adapter", + "--operation", + "validate", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "resourceTypeArg": "--resource-type" + } + ] + } +} diff --git a/tools/dsctest/src/adapter.rs b/tools/dsctest/src/adapter.rs new file mode 100644 index 000000000..d634c541a --- /dev/null +++ b/tools/dsctest/src/adapter.rs @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::args::AdapterOperation; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct AdaptedOne { + pub one: String, + #[serde(rename = "_name", skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct AdaptedTwo { + pub two: String, + #[serde(rename = "_name", skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct DscResource { + /// The namespaced name of the resource. + #[serde(rename="type")] + pub type_name: String, + /// The kind of resource. + pub kind: String, + /// The version of the resource. + pub version: String, + /// The capabilities of the resource. + pub capabilities: Vec, + /// The file path to the resource. + pub path: String, + /// The directory path to the resource. + pub directory: String, + /// The implementation of the resource. + #[serde(rename="implementedAs")] + pub implemented_as: String, + /// The properties of the resource. + pub properties: Vec, + /// The required resource adapter for the resource. + #[serde(rename="requireAdapter")] + pub require_adapter: Option, +} + +pub fn adapt(resource_type: &str, input: &str, operation: &AdapterOperation) -> Result { + match operation { + AdapterOperation::List => { + let resource_one = DscResource { + type_name: "Adapted/One".to_string(), + kind: "resource".to_string(), + version: "1.0.0".to_string(), + capabilities: vec!["get".to_string(), "set".to_string(), "test".to_string(), "export".to_string()], + path: "path/to/adapted/one".to_string(), + directory: "path/to/adapted".to_string(), + implemented_as: "TestAdapted".to_string(), + properties: vec!["one".to_string()], + require_adapter: Some("Test/Adapter".to_string()), + }; + let resource_two = DscResource { + type_name: "Adapted/Two".to_string(), + kind: "resource".to_string(), + version: "1.0.0".to_string(), + capabilities: vec!["get".to_string(), "set".to_string(), "test".to_string(), "export".to_string()], + path: "path/to/adapted/two".to_string(), + directory: "path/to/adapted".to_string(), + implemented_as: "TestAdapted".to_string(), + properties: vec!["two".to_string()], + require_adapter: Some("Test/Adapter".to_string()), + }; + println!("{}", serde_json::to_string(&resource_one).unwrap()); + println!("{}", serde_json::to_string(&resource_two).unwrap()); + std::process::exit(0); + }, + AdapterOperation::Get => { + match resource_type { + "Adapted/One" => { + let adapted_one = AdaptedOne { + one: "value1".to_string(), + name: None, + }; + Ok(serde_json::to_string(&adapted_one).unwrap()) + }, + "Adapted/Two" => { + let adapted_two = AdaptedTwo { + two: "value2".to_string(), + name: None, + }; + Ok(serde_json::to_string(&adapted_two).unwrap()) + }, + _ => Err(format!("Unknown resource type: {resource_type}")), + } + }, + AdapterOperation::Set | AdapterOperation::Test => { + match resource_type { + "Adapted/One" => { + let adapted_one: AdaptedOne = serde_json::from_str(input) + .map_err(|e| format!("Failed to parse input for Adapted/One: {e}"))?; + Ok(serde_json::to_string(&adapted_one).unwrap()) + }, + "Adapted/Two" => { + let adapted_two: AdaptedTwo = serde_json::from_str(input) + .map_err(|e| format!("Failed to parse input for Adapted/Two: {e}"))?; + Ok(serde_json::to_string(&adapted_two).unwrap()) + }, + _ => Err(format!("Unknown resource type: {resource_type}")), + } + }, + AdapterOperation::Export => { + match resource_type { + "Adapted/One" => { + let adapted_one = AdaptedOne { + one: "first1".to_string(), + name: Some("first".to_string()), + }; + println!("{}", serde_json::to_string(&adapted_one).unwrap()); + let adapted_one = AdaptedOne { + one: "second1".to_string(), + name: Some("second".to_string()), + }; + println!("{}", serde_json::to_string(&adapted_one).unwrap()); + std::process::exit(0); + }, + "Adapted/Two" => { + let adapted_two = AdaptedTwo { + two: "first2".to_string(), + name: Some("first".to_string()), + }; + println!("{}", serde_json::to_string(&adapted_two).unwrap()); + let adapted_two = AdaptedTwo { + two: "second2".to_string(), + name: Some("second".to_string()), + }; + println!("{}", serde_json::to_string(&adapted_two).unwrap()); + std::process::exit(0); + }, + _ => Err(format!("Unknown resource type: {resource_type}")), + } + }, + AdapterOperation::Validate => { + Ok("{\"valid\": true}".to_string()) + }, + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index b0a31d9b4..f2054d99b 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -5,6 +5,7 @@ use clap::{Parser, Subcommand, ValueEnum}; #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] pub enum Schemas { + Adapter, Delete, Exist, ExitCode, @@ -27,8 +28,28 @@ pub struct Args { pub subcommand: SubCommand, } +#[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] +pub enum AdapterOperation { + Get, + Set, + Test, + List, + Export, + Validate, +} + #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum SubCommand { + #[clap(name = "adapter", about = "Resource adapter")] + Adapter { + #[clap(name = "input", short, long, help = "The input to the adapter command as JSON")] + input: String, + #[clap(name = "resource-type", short, long, help = "The resource type to adapt to")] + resource_type: String, + #[clap(name = "operation", short, long, help = "The operation to perform")] + operation: AdapterOperation, + }, + #[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/main.rs b/tools/dsctest/src/main.rs index 6228a6709..8d96d9de1 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -10,6 +10,7 @@ mod exporter; mod get; mod in_desired_state; mod metadata; +mod adapter; mod sleep; mod trace; mod version; @@ -37,6 +38,15 @@ use std::{thread, time::Duration}; fn main() { let args = Args::parse(); let json = match args.subcommand { + SubCommand::Adapter { input , resource_type, operation } => { + match adapter::adapt(&resource_type, &input, &operation) { + Ok(result) => result, + Err(err) => { + eprintln!("Error adapting resource: {err}"); + std::process::exit(1); + } + } + }, SubCommand::Delete { input } => { let mut delete = match serde_json::from_str::(&input) { Ok(delete) => delete, @@ -162,7 +172,7 @@ fn main() { instances.into_iter().next().unwrap_or_else(|| { eprintln!("No instances found"); std::process::exit(1); - }) + }) } }; serde_json::to_string(&resource).unwrap() @@ -201,6 +211,9 @@ fn main() { }, SubCommand::Schema { subcommand } => { let schema = match subcommand { + Schemas::Adapter => { + schema_for!(adapter::DscResource) + }, Schemas::Delete => { schema_for!(Delete) }, diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index ba98b32e6..04f753ffc 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -25,6 +25,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: vec!["Property1".to_string(), "Property2".to_string()], require_adapter: Some("Test/TestGroup".to_string()), + target_resource: None, manifest: Some(serde_json::to_value(ResourceManifest { description: Some("This is a test resource.".to_string()), schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), @@ -50,6 +51,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: vec!["Property1".to_string(), "Property2".to_string()], require_adapter: Some("Test/TestGroup".to_string()), + target_resource: None, manifest: Some(serde_json::to_value(ResourceManifest { description: Some("This is a test resource.".to_string()), schema_version: dsc_lib::dscresources::resource_manifest::ResourceManifest::default_schema_id_uri(), @@ -79,6 +81,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: vec!["Property1".to_string(), "Property2".to_string()], require_adapter: None, + target_resource: None, manifest: None, }; println!("{}", serde_json::to_string(&resource1).unwrap());