diff --git a/dsc/examples/powershell.dsc.yaml b/dsc/examples/powershell.dsc.yaml index d649d8de..3bb47d62 100644 --- a/dsc/examples/powershell.dsc.yaml +++ b/dsc/examples/powershell.dsc.yaml @@ -1,16 +1,19 @@ # Example configuration mixing native app resources with classic PS resources $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +metadata: + Microsoft.DSC: + securityContext: elevated resources: - name: Use class PowerShell resources - type: Microsoft.DSC/PowerShell + type: Microsoft.Windows/WindowsPowerShell properties: resources: - name: OpenSSH service - type: PsDesiredStateConfiguration/MSFT_ServiceResource + type: PsDesiredStateConfiguration/Service properties: Name: sshd - name: Administrator - type: PsDesiredStateConfiguration/MSFT_UserResource + type: PsDesiredStateConfiguration/User properties: UserName: administrator - name: current user registry @@ -18,4 +21,4 @@ resources: properties: keyPath: HKLM\Software\Microsoft\Windows NT\CurrentVersion valueName: ProductName - _ensure: Present + _exist: True diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index aca7372d..740ab9a1 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -67,7 +67,6 @@ noParameters = "No parameters specified" [resource_command] implementedAs = "implemented as" invalidOperationOnAdapter = "Can not perform this operation on the adapter itself" -adapterNotFound = "Adapter not found" setInputEmpty = "Desired input is empty" testInputEmpty = "Expected input is required" diff --git a/dsc/src/resource_command.rs b/dsc/src/resource_command.rs index d33df5b8..13c7f748 100644 --- a/dsc/src/resource_command.rs +++ b/dsc/src/resource_command.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use crate::args::OutputFormat; -use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, EXIT_DSC_RESOURCE_NOT_FOUND, add_type_name_to_json, write_object}; +use crate::util::{EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_JSON_ERROR, EXIT_DSC_RESOURCE_NOT_FOUND, write_object}; use dsc_lib::configure::config_doc::{Configuration, ExecutionKind}; use dsc_lib::configure::add_resource_export_results_to_configuration; use dsc_lib::dscresources::{resource_manifest::Kind, invoke_result::{GetResult, ResourceGetResponse}}; @@ -16,8 +16,8 @@ use dsc_lib::{ }; use std::process::exit; -pub fn get(dsc: &DscManager, resource_type: &str, mut input: String, format: Option<&OutputFormat>) { - let Some(mut resource) = get_resource(dsc, resource_type) else { +pub fn get(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) { + let Some(resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -28,17 +28,7 @@ pub fn get(dsc: &DscManager, resource_type: &str, mut input: String, format: Opt exit(EXIT_DSC_ERROR); } - if let Some(requires) = &resource.require_adapter { - input = add_type_name_to_json(input, resource.type_name.clone()); - if let Some(pr) = get_resource(dsc, requires) { - resource = pr; - } else { - error!("{}: {requires}", t!("resource_command.adapterNotFound")); - return; - }; - } - - match resource.get(input.as_str()) { + match resource.get(input) { Ok(result) => { // convert to json let json = match serde_json::to_string(&result) { @@ -58,8 +48,8 @@ pub fn get(dsc: &DscManager, resource_type: &str, mut input: String, format: Opt } pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&OutputFormat>) { - let mut input = String::new(); - let Some(mut resource) = get_resource(dsc, resource_type) else { + let input = String::new(); + let Some(resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -70,16 +60,6 @@ pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&OutputForm exit(EXIT_DSC_ERROR); } - if let Some(requires) = &resource.require_adapter { - input = add_type_name_to_json(input, resource.type_name.clone()); - if let Some(pr) = get_resource(dsc, requires) { - resource = pr; - } else { - error!("{}: {requires}", t!("resource_command.adapterNotFound")); - return; - }; - } - let export_result = match resource.export(&input) { Ok(export) => { export } Err(err) => { @@ -107,13 +87,13 @@ pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&OutputForm } } -pub fn set(dsc: &DscManager, resource_type: &str, mut input: String, format: Option<&OutputFormat>) { +pub fn set(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) { if input.is_empty() { error!("{}", t!("resource_command.setInputEmpty")); exit(EXIT_INVALID_ARGS); } - let Some(mut resource) = get_resource(dsc, resource_type) else { + let Some(resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -124,17 +104,7 @@ pub fn set(dsc: &DscManager, resource_type: &str, mut input: String, format: Opt exit(EXIT_DSC_ERROR); } - if let Some(requires) = &resource.require_adapter { - input = add_type_name_to_json(input, resource.type_name.clone()); - if let Some(pr) = get_resource(dsc, requires) { - resource = pr; - } else { - error!("{}: {requires}", t!("resource_command.adapterNotFound")); - return; - }; - } - - match resource.set(input.as_str(), true, &ExecutionKind::Actual) { + match resource.set(input, true, &ExecutionKind::Actual) { Ok(result) => { // convert to json let json = match serde_json::to_string(&result) { @@ -153,13 +123,13 @@ pub fn set(dsc: &DscManager, resource_type: &str, mut input: String, format: Opt } } -pub fn test(dsc: &DscManager, resource_type: &str, mut input: String, format: Option<&OutputFormat>) { +pub fn test(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) { if input.is_empty() { error!("{}", t!("resource_command.testInputEmpty")); exit(EXIT_INVALID_ARGS); } - let Some(mut resource) = get_resource(dsc, resource_type) else { + let Some(resource) = get_resource(dsc, resource_type) else { error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -170,17 +140,7 @@ pub fn test(dsc: &DscManager, resource_type: &str, mut input: String, format: Op exit(EXIT_DSC_ERROR); } - if let Some(requires) = &resource.require_adapter { - input = add_type_name_to_json(input, resource.type_name.clone()); - if let Some(pr) = get_resource(dsc, requires) { - resource = pr; - } else { - error!("{}: {requires}", t!("resource_command.adapterNotFound")); - return; - }; - } - - match resource.test(input.as_str()) { + match resource.test(input) { Ok(result) => { // convert to json let json = match serde_json::to_string(&result) { @@ -199,8 +159,8 @@ pub fn test(dsc: &DscManager, resource_type: &str, mut input: String, format: Op } } -pub fn delete(dsc: &DscManager, resource_type: &str, mut input: String) { - let Some(mut resource) = get_resource(dsc, resource_type) else { +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()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); }; @@ -211,17 +171,7 @@ pub fn delete(dsc: &DscManager, resource_type: &str, mut input: String) { exit(EXIT_DSC_ERROR); } - if let Some(requires) = &resource.require_adapter { - input = add_type_name_to_json(input, resource.type_name.clone()); - if let Some(pr) = get_resource(dsc, requires) { - resource = pr; - } else { - error!("{}: {requires}", t!("resource_command.adapterNotFound")); - return; - }; - } - - match resource.delete(input.as_str()) { + match resource.delete(input) { Ok(()) => {} Err(err) => { error!("Error: {err}"); @@ -259,7 +209,7 @@ pub fn schema(dsc: &DscManager, resource_type: &str, format: Option<&OutputForma } } -pub fn export(dsc: &mut DscManager, resource_type: &str, mut input: String, format: Option<&OutputFormat>) { +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()); exit(EXIT_DSC_RESOURCE_NOT_FOUND); @@ -270,20 +220,8 @@ pub fn export(dsc: &mut DscManager, resource_type: &str, mut input: String, form exit(EXIT_DSC_ERROR); } - let mut adapter_resource: Option<&DscResource> = None; - if let Some(requires) = &dsc_resource.require_adapter { - input = add_type_name_to_json(input, dsc_resource.type_name.clone()); - if let Some(pr) = get_resource(dsc, requires) { - adapter_resource = Some(pr); - } else { - error!("{}: {requires}", t!("resource_command.adapterNotFound")); - return; - }; - } - let mut conf = Configuration::new(); - - if let Err(err) = add_resource_export_results_to_configuration(dsc_resource, adapter_resource, &mut conf, &input) { + if let Err(err) = add_resource_export_results_to_configuration(dsc_resource, &mut conf, input) { error!("{err}"); exit(EXIT_DSC_ERROR); } diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index c271ff0f..5c9c3616 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -548,30 +548,30 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat ResourceSubCommand::Export { resource, input, file, output_format } => { dsc.find_resources(&[resource.to_string()], progress_format); let parsed_input = get_input(input.as_ref(), file.as_ref()); - resource_command::export(&mut dsc, resource, parsed_input, output_format.as_ref()); + resource_command::export(&mut dsc, resource, &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Get { resource, input, file: path, all, output_format } => { dsc.find_resources(&[resource.to_string()], progress_format); if *all { resource_command::get_all(&dsc, resource, output_format.as_ref()); } else { let parsed_input = get_input(input.as_ref(), path.as_ref()); - resource_command::get(&dsc, resource, parsed_input, output_format.as_ref()); + resource_command::get(&dsc, resource, &parsed_input, output_format.as_ref()); } }, ResourceSubCommand::Set { resource, input, file: path, output_format } => { dsc.find_resources(&[resource.to_string()], progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref()); - resource_command::set(&dsc, resource, parsed_input, output_format.as_ref()); + resource_command::set(&dsc, resource, &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Test { resource, input, file: path, output_format } => { dsc.find_resources(&[resource.to_string()], progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref()); - resource_command::test(&dsc, resource, parsed_input, output_format.as_ref()); + resource_command::test(&dsc, resource, &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Delete { resource, input, file: path } => { dsc.find_resources(&[resource.to_string()], progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref()); - resource_command::delete(&dsc, resource, parsed_input); + resource_command::delete(&dsc, resource, &parsed_input); }, } } diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 454f7e6a..3c1607fa 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -129,37 +129,6 @@ pub fn add_fields_to_json(json: &str, fields_to_add: &HashMap) - Ok(result) } -/// Add the type property value to the JSON. -/// -/// # Arguments -/// -/// * `json` - The JSON to add the type property to -/// * `type_name` - The type name to add -/// -/// # Returns -/// -/// * `String` - The JSON with the type property added -#[must_use] -pub fn add_type_name_to_json(json: String, type_name: String) -> String -{ - let mut map:HashMap = HashMap::new(); - map.insert(String::from("adapted_dsc_type"), type_name); - - let mut j = json; - if j.is_empty() - { - j = String::from("{}"); - } - - match add_fields_to_json(&j, &map) { - Ok(json) => json, - Err(err) => { - error!("JSON: {err}"); - exit(EXIT_JSON_ERROR); - } - } -} - /// Get the JSON schema for requested type. /// /// # Arguments diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 5aca0d8d..32a7d1c5 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -35,7 +35,6 @@ circularDependency = "Circular dependency detected for resource named '%{resourc invocationOrder = "Resource invocation order" [configure.mod] -escapePropertyValues = "Escape returned property values" nestedArraysNotSupported = "Nested arrays not supported" arrayElementCouldNotTransformAsString = "Array element could not be transformed as string" valueCouldNotBeTransformedAsString = "Property value '%{value}' could not be transformed as string" @@ -89,7 +88,7 @@ invokeGet = "Invoking get for '%{resource}'" invokeGetUsing = "Invoking get '%{resource}' using '%{executable}'" verifyOutputUsing = "Verifying output of get '%{resource}' using '%{executable}'" groupGetResponse = "Group get response: %{response}" -failedParseJson = "Failed to parse JSON from get %{executable}|%{stdout}|%{stderr} -> %{err}" +failedParseJson = "Failed to parse JSON from 'get': executable = '%{executable}' stdout = '%{stdout}' stderr = '%{stderr}' -> %{err}" invokeSet = "Invoking set for '%{resource}'" noPretest = "No pretest, invoking test on '%{resource}'" syntheticWhatIf = "cannot process what-if execution type, as resource implements pre-test and does not support what-if" @@ -139,9 +138,16 @@ invokeSet = "Invoking set for '%{resource}'" invokeTest = "Invoking test for '%{resource}'" invokeDelete = "Invoking delete for '%{resource}'" invokeValidate = "Invoking validate for '%{resource}'" +invokeValidateNotSupported = "Invoking validate is not supported for adapted resource '%{resource}'" invokeSchema = "Invoking schema for '%{resource}'" +invokeSchemaNotSupported = "Invoking schema is not supported for adapted resource '%{resource}'" invokeExport = "Invoking export for '%{resource}'" +invokeExportReturnedNoResult = "Invoking export returned no result for '%{resource}'" invokeResolve = "Invoking resolve for '%{resource}'" +invokeResolveNotSupported = "Invoking resolve is not supported for adapted resource '%{resource}'" +invokeReturnedWrongResult = "Invoking '%{operation}' on '%{resource}' returned unexpected result" +propertyIncorrectType = "Property '%{property}' is not of type '%{property_type}'" +propertyNotFound = "Property '%{property}' not found" subDiff = "diff: sub diff for '%{key}'" diffArray = "diff: arrays differ for '%{key}'" diffNotArray = "diff: '%{key}' is not an array" diff --git a/dsc_lib/src/configure/config_doc.rs b/dsc_lib/src/configure/config_doc.rs index a0e6edac..b841bf2a 100644 --- a/dsc_lib/src/configure/config_doc.rs +++ b/dsc_lib/src/configure/config_doc.rs @@ -9,13 +9,6 @@ use std::collections::HashMap; use crate::{dscerror::DscError, schemas::DscRepoSchema}; -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -#[serde(rename_all = "camelCase")] -pub enum ContextKind { - Configuration, - Resource, -} - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub enum SecurityContextKind { @@ -63,9 +56,6 @@ pub struct MicrosoftDscMetadata { /// The security context of the configuration operation, can be specified to be required #[serde(rename = "securityContext", skip_serializing_if = "Option::is_none")] pub security_context: Option, - /// Identifies if the operation is part of a configuration - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index d2004873..74bcea0e 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -55,12 +55,9 @@ pub struct Configurator { /// # Errors /// /// This function will return an error if the underlying resource fails. -pub fn add_resource_export_results_to_configuration(resource: &DscResource, adapter_resource: Option<&DscResource>, conf: &mut Configuration, input: &str) -> Result { +pub fn add_resource_export_results_to_configuration(resource: &DscResource, conf: &mut Configuration, input: &str) -> Result { - let export_result = match adapter_resource { - Some(_) => adapter_resource.unwrap().export(input)?, - _ => resource.export(input)? - }; + let export_result = resource.export(input)?; if resource.kind == Kind::Exporter { for instance in &export_result.actual_state { @@ -85,7 +82,6 @@ pub fn add_resource_export_results_to_configuration(resource: &DscResource, adap // for values returned by resources, they may look like expressions, so we make sure to escape them in case // they are re-used to apply configuration fn escape_property_values(properties: &Map) -> Result>, DscError> { - debug!("{}", t!("configure.mod.escapePropertyValues")); let mut result: Map = Map::new(); for (name, value) in properties { match value { @@ -566,7 +562,7 @@ impl Configurator { let properties = self.get_properties(resource, &dsc_resource.kind)?; let input = add_metadata(&dsc_resource.kind, properties)?; trace!("{}", t!("configure.mod.exportInput", input = input)); - let export_result = match add_resource_export_results_to_configuration(dsc_resource, Some(dsc_resource), &mut conf, input.as_str()) { + let export_result = match add_resource_export_results_to_configuration(dsc_resource, &mut conf, input.as_str()) { Ok(result) => result, Err(e) => { progress.set_failure(get_failure_from_error(&e)); @@ -713,7 +709,6 @@ impl Configurator { Metadata { microsoft: Some( MicrosoftDscMetadata { - context: None, version: Some(env!("CARGO_PKG_VERSION").to_string()), operation: Some(operation), execution_type: Some(self.context.execution_type.clone()), diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index bfef5072..d69d4c8f 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::{configure::config_doc::ExecutionKind, dscresources::resource_manifest::Kind}; +use crate::{configure::{config_doc::{Configuration, ExecutionKind, Resource}, Configurator}, dscresources::resource_manifest::Kind}; +use crate::dscresources::invoke_result::{ResourceGetResponse, ResourceSetResponse}; use dscerror::DscError; use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Map, Value}; use std::collections::HashMap; use tracing::{debug, info}; @@ -92,6 +93,31 @@ impl DscResource { manifest: None, } } + + fn create_config_for_adapter(self, adapter: &str, input: &str) -> Result { + // create new configuration with adapter and use this as the resource + let mut configuration = Configuration::new(); + let mut property_map = Map::new(); + property_map.insert("name".to_string(), Value::String(self.type_name.clone())); + property_map.insert("type".to_string(), Value::String(self.type_name.clone())); + if !input.is_empty() { + let resource_properties: Value = serde_json::from_str(input)?; + property_map.insert("properties".to_string(), resource_properties); + } + let mut resources_map = Map::new(); + resources_map.insert("resources".to_string(), Value::Array(vec![Value::Object(property_map)])); + let adapter_resource = Resource { + name: self.type_name.clone(), + resource_type: adapter.to_string(), + depends_on: None, + metadata: None, + properties: Some(resources_map), + }; + configuration.resources.push(adapter_resource); + let config_json = serde_json::to_string(&configuration)?; + let configurator = Configurator::new(&config_json, crate::progress::ProgressFormat::None)?; + Ok(configurator) + } } impl Default for DscResource { @@ -191,6 +217,24 @@ pub trait Invoke { impl Invoke for DscResource { fn get(&self, filter: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeGet", resource = self.type_name)); + if let Some(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); + } + match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -207,6 +251,33 @@ 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("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 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); + } + match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -223,6 +294,34 @@ 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 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, + }); + return Ok(test_result); + } + match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -267,6 +366,12 @@ 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(()); + } + match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -283,6 +388,10 @@ impl Invoke for DscResource { fn validate(&self, config: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeValidate", resource = self.type_name)); + if self.require_adapter.is_some() { + return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeValidateNotSupported", resource = self.type_name).to_string())); + } + match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -299,6 +408,10 @@ impl Invoke for DscResource { fn schema(&self) -> Result { debug!("{}", t!("dscresources.dscresource.invokeSchema", resource = self.type_name)); + if self.require_adapter.is_some() { + return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeSchemaNotSupported", resource = self.type_name).to_string())); + } + match &self.implemented_as { ImplementedAs::Custom(_custom) => { Err(DscError::NotImplemented(t!("dscresources.dscresource.customResourceNotSupported").to_string())) @@ -315,6 +428,24 @@ 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(), + }; + 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())); + }; + export_result.actual_state.push(serde_json::to_value(properties["properties"].clone())?); + } + return Ok(export_result); + } + let Some(manifest) = &self.manifest else { return Err(DscError::MissingManifest(self.type_name.clone())); }; @@ -324,6 +455,10 @@ impl Invoke for DscResource { fn resolve(&self, input: &str) -> Result { debug!("{}", t!("dscresources.dscresource.invokeResolve", resource = self.type_name)); + if self.require_adapter.is_some() { + return Err(DscError::NotSupported(t!("dscresources.dscresource.invokeResolveNotSupported", resource = self.type_name).to_string())); + } + let Some(manifest) = &self.manifest else { return Err(DscError::MissingManifest(self.type_name.clone())); }; diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index da66b5cc..9d7404a1 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -136,7 +136,7 @@ pub struct ValidateResult { #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] pub struct ExportResult { - /// The state of the resource as it was returned by the Get method. + /// The state of the resource as it was returned by the Export method. #[serde(rename = "actualState")] pub actual_state: Vec, } diff --git a/powershell-adapter/Tests/TestAdapter/testadapter.dsc.resource.json b/powershell-adapter/Tests/TestAdapter/testadapter.dsc.resource.json index 8623253e..e05c8506 100644 --- a/powershell-adapter/Tests/TestAdapter/testadapter.dsc.resource.json +++ b/powershell-adapter/Tests/TestAdapter/testadapter.dsc.resource.json @@ -41,8 +41,7 @@ "$Input | ./testadapter.resource.ps1 Set" ], "input": "stdin", - "implementsPretest": true, - "return": "state" + "implementsPretest": true }, "test": { "executable": "pwsh", @@ -53,8 +52,7 @@ "-Command", "$Input | ./testadapter.resource.ps1 Test" ], - "input": "stdin", - "return": "state" + "input": "stdin" }, "export": { "executable": "pwsh", @@ -65,8 +63,7 @@ "-Command", "$Input | ./testadapter.resource.ps1 Export" ], - "input": "stdin", - "return": "state" + "input": "stdin" }, "validate": { "executable": "pwsh", diff --git a/powershell-adapter/Tests/TestAdapter/testadapter.resource.ps1 b/powershell-adapter/Tests/TestAdapter/testadapter.resource.ps1 index a518dadc..72bebe4c 100644 --- a/powershell-adapter/Tests/TestAdapter/testadapter.resource.ps1 +++ b/powershell-adapter/Tests/TestAdapter/testadapter.resource.ps1 @@ -30,7 +30,7 @@ if ($jsonInput -ne '@{}') { $inputobj = $jsonInput | ConvertFrom-Json } -$jsonInput | Write-DscTrace +"Input: $jsonInput" | Write-DscTrace switch ($Operation) { 'List' { @@ -48,16 +48,19 @@ switch ($Operation) { description = 'TestCase resource' } | ConvertTo-Json -Compress } - { @('Get','Set','Test','Export') -contains $_ } { + { @('Get','Set','Test') -contains $_ } { + "Operation: $Operation" | Write-DscTrace - # TestCase 1 = 'Verify adapted_dsc_type field' - if (($inputobj.TestCaseId -eq 1 ) -or ($_ -eq 'Export')){ - $result = $inputobj.adapted_dsc_type -eq 'Test/TestCase' - $result = @{'TestCaseId'=1; 'Input'=''; result = $result } | ConvertTo-Json -Depth 10 -Compress - return $result + if ($inputobj.resources.properties.TestCaseId -eq 1) { + "Is TestCaseId 1" | Write-DscTrace + @{result = @(@{name = $inputobj.resources.name; type = $inputobj.resources.type; properties = @{'TestCaseId' = 1; 'Input' = ''}})} | ConvertTo-Json -Depth 10 -Compress } } + 'Export' { + @(@{name = $inputobj.resources.name; type = $inputobj.resources.type; properties = @{'TestCaseId' = 1; 'Input' = ''}}) | ConvertTo-Json -Depth 10 -Compress + @(@{name = $inputobj.resources.name; type = $inputobj.resources.type; properties = @{'TestCaseId' = 2; 'Input' = ''}}) | ConvertTo-Json -Depth 10 -Compress + } 'Validate' { @{ valid = $true } | ConvertTo-Json } diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index 583bfbfc..23d52e75 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -46,7 +46,7 @@ Describe 'PowerShell adapter resource tests' { '@ $yaml | dsc -l trace config get -f - 2> "$TestDrive/tracing.txt" $LASTEXITCODE | Should -Be 2 - "$TestDrive/tracing.txt" | Should -FileContentMatch 'DSC resource TestClassResourceNotExist/TestClassResourceNotExist module not found.' + "$TestDrive/tracing.txt" | Should -FileContentMatch "DSC resource 'TestClassResourceNotExist/TestClassResourceNotExist' module not found." } It 'Test works on config with class-based resources' { @@ -202,4 +202,39 @@ Describe 'PowerShell adapter resource tests' { $LASTEXITCODE | Should -Be 0 $out.results.result.actualState.result.properties.HashTableProp.Name | Should -BeExactly 'DSCv3' } + + It 'Config calling PS Resource directly works for ' -TestCases @( + @{ Operation = 'get' } + @{ Operation = 'set' } + @{ Operation = 'test' } + ) { + param($Operation) + + $yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Class-resource Info + type: TestClassResource/TestClassResource + properties: + Name: 'TestClassResource1' + HashTableProp: + Name: 'DSCv3' +"@ + + $out = dsc config $operation -i $yaml | ConvertFrom-Json + $text = dsc config $operation -i $yaml | Out-String + $LASTEXITCODE | Should -Be 0 + switch ($Operation) { + 'get' { + $out.results[0].result.actualState.Name | Should -BeExactly 'TestClassResource1' -Because $text + } + 'set' { + $out.results[0].result.beforeState.Name | Should -BeExactly 'TestClassResource1' -Because $text + $out.results[0].result.afterState.Name | Should -BeExactly 'TestClassResource1' -Because $text + } + 'test' { + $out.results[0].result.actualState.InDesiredState | Should -BeFalse -Because $text + } + } + } } diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index abdc9dce..c8be165e 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -35,10 +35,10 @@ Describe 'PowerShell adapter resource tests' { $r = "{'Name':'TestClassResource1'}" | dsc resource get -r 'TestClassResource/TestClassResource' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.result.properties.Prop1 | Should -BeExactly 'ValueForProp1' + $res.actualState.Prop1 | Should -BeExactly 'ValueForProp1' # verify that only properties with DscProperty attribute are returned - $propertiesNames = $res.actualState.result.properties | Get-Member -MemberType NoteProperty | % Name + $propertiesNames = $res.actualState | Get-Member -MemberType NoteProperty | % Name $propertiesNames | Should -Not -Contain 'NonDscProperty' $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' } @@ -48,7 +48,7 @@ Describe 'PowerShell adapter resource tests' { $r = "{'Name':'TestClassResource1'}" | dsc resource get -r 'TestClassResource/TestClassResource' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.result.properties.EnumProp | Should -BeExactly 'Expected' + $res.actualState.EnumProp | Should -BeExactly 'Expected' } It 'Test works on class-based resource' { @@ -56,11 +56,11 @@ Describe 'PowerShell adapter resource tests' { $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource test -r 'TestClassResource/TestClassResource' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.result.properties.InDesiredState | Should -Be $True - $res.actualState.result.properties.InDesiredState.GetType().Name | Should -Be "Boolean" + $res.actualState.InDesiredState | Should -Be $True + $res.actualState.InDesiredState.GetType().Name | Should -Be "Boolean" # verify that only properties with DscProperty attribute are returned - $propertiesNames = $res.actualState.result.properties.InDesiredState | Get-Member -MemberType NoteProperty | % Name + $propertiesNames = $res.actualState.InDesiredState | Get-Member -MemberType NoteProperty | % Name $propertiesNames | Should -Not -Contain 'NonDscProperty' $propertiesNames | Should -Not -Contain 'HiddenNonDscProperty' } @@ -70,10 +70,11 @@ Describe 'PowerShell adapter resource tests' { $r = "{'Name':'TestClassResource1','Prop1':'ValueForProp1'}" | dsc resource set -r 'TestClassResource/TestClassResource' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.afterState.result | Should -Not -BeNull + $res.afterState.Prop1 | Should -BeExactly 'ValueForProp1' + $res.changedProperties | Should -BeNullOrEmpty } - It 'Export works on PS class-based resource' { + It 'Export works on PS class-based resource' -Pending { $r = dsc resource export -r TestClassResource/TestClassResource $LASTEXITCODE | Should -Be 0 @@ -90,7 +91,7 @@ Describe 'PowerShell adapter resource tests' { } } - It 'Get --all works on PS class-based resource' { + It 'Get --all works on PS class-based resource' -Pending { $r = dsc resource get --all -r TestClassResource/TestClassResource $LASTEXITCODE | Should -Be 0 @@ -224,81 +225,89 @@ Describe 'PowerShell adapter resource tests' { } } - It 'Verify adapted_dsc_type field in Get' { + It 'Verify invoke Get on adapted resource' { $oldPath = $env:PATH try { $adapterPath = Join-Path $PSScriptRoot 'TestAdapter' $env:PATH += [System.IO.Path]::PathSeparator + $adapterPath - $r = '{TestCaseId: 1}' | dsc resource get -r 'Test/TestCase' -f - - $LASTEXITCODE | Should -Be 0 + $r = '{"TestCaseId": 1}' | dsc -l trace resource get -r 'Test/TestCase' -f - 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/tracing.txt | Out-String) $resources = $r | ConvertFrom-Json - $resources.actualState.result | Should -Be $True + $resources.actualState.TestCaseid | Should -Be 1 } finally { $env:PATH = $oldPath } } - It 'Verify adapted_dsc_type field in Set' { + It 'Verify invoke Set on adapted resource' { $oldPath = $env:PATH try { $adapterPath = Join-Path $PSScriptRoot 'TestAdapter' $env:PATH += [System.IO.Path]::PathSeparator + $adapterPath - $r = '{TestCaseId: 1}' | dsc resource set -r 'Test/TestCase' -f - - $LASTEXITCODE | Should -Be 0 + $r = '{"TestCaseId": 1}' | dsc resource set -r 'Test/TestCase' -f - 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/tracing.txt | Out-String) $resources = $r | ConvertFrom-Json - $resources.beforeState.result | Should -Be $True - $resources.afterState.result | Should -Be $True + $resources.beforeState.TestCaseid | Should -Be 1 + $resources.afterState.TestCaseId | Should -Be 1 } finally { $env:PATH = $oldPath } } - It 'Verify adapted_dsc_type field in Test' { + It 'Verify invoke Test on adapted resource' { $oldPath = $env:PATH try { $adapterPath = Join-Path $PSScriptRoot 'TestAdapter' $env:PATH += [System.IO.Path]::PathSeparator + $adapterPath - $r = '{TestCaseId: 1}' | dsc resource test -r 'Test/TestCase' -f - + $r = '{"TestCaseId": 1}' | dsc resource test -r 'Test/TestCase' -f - $LASTEXITCODE | Should -Be 0 $resources = $r | ConvertFrom-Json - $resources.actualState.result | Should -Be $True + $resources.actualState.TestCaseId | Should -Be 1 } finally { $env:PATH = $oldPath } } - It 'Verify adapted_dsc_type field in Export' { + It 'Verify invoke Export on adapted resource' { $oldPath = $env:PATH try { $adapterPath = Join-Path $PSScriptRoot 'TestAdapter' $env:PATH += [System.IO.Path]::PathSeparator + $adapterPath - $r = dsc resource export -r 'Test/TestCase' - $LASTEXITCODE | Should -Be 0 + $r = dsc -l trace resource export -r 'Test/TestCase' 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/tracing.txt | Out-String) $resources = $r | ConvertFrom-Json - $resources.resources[0].properties.result | Should -Be $True + $resources.resources.count | Should -Be 2 + $resources.resources[0].type | Should -BeExactly 'Test/TestCase' + $resources.resources[0].name | Should -BeExactly 'Test/TestCase-0' + $resources.resources[0].properties.TestCaseId | Should -Be 1 + $resources.resources[1].type | Should -BeExactly 'Test/TestCase' + $resources.resources[1].name | Should -BeExactly 'Test/TestCase-1' + $resources.resources[1].properties.TestCaseId | Should -Be 2 } finally { $env:PATH = $oldPath } } - It 'Dsc can process large resource output' { - $env:TestClassResourceResultCount = 5000 # with sync resource invocations this was not possible - - dsc resource list -a Microsoft.DSC/PowerShell | Out-Null - $r = dsc resource export -r TestClassResource/TestClassResource - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.resources[0].properties.result.count | Should -Be 5000 + It 'Dsc can process large resource output' -Pending { + try { + $env:TestClassResourceResultCount = 5000 # with sync resource invocations this was not possible - $env:TestClassResourceResultCount = $null + $r = dsc -l trace resource export -r TestClassResource/TestClassResource 2> $TestDrive/tracing.txt + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/tracing.txt | Out-String) + $res = $r | ConvertFrom-Json + $res.resources[0].properties.result.count | Should -Be 5000 + } + finally { + $env:TestClassResourceResultCount = $null + } } It 'Verify that there are no cache rebuilds for several sequential executions' { @@ -334,7 +343,6 @@ Describe 'PowerShell adapter resource tests' { $r = '{"HashTableProp":{"Name":"DSCv3"},"Name":"TestClassResource1"}' | dsc resource get -r 'TestClassResource/TestClassResource' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.result.properties.HashTableProp.Name | Should -Be 'DSCv3' - + $res.actualState.HashTableProp.Name | Should -Be 'DSCv3' } } diff --git a/powershell-adapter/Tests/win_powershellgroup.tests.ps1 b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 index 4a3a9035..20800c69 100644 --- a/powershell-adapter/Tests/win_powershellgroup.tests.ps1 +++ b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 @@ -40,7 +40,7 @@ Describe 'WindowsPowerShell adapter resource tests - requires elevated permissio $r = '{"DestinationPath":"' + $testFile.replace('\','\\') + '"}' | dsc resource get -r 'PSDesiredStateConfiguration/File' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.result.properties.DestinationPath | Should -Be "$testFile" + $res.actualState.DestinationPath | Should -Be "$testFile" } It 'Set works on Binary "File" resource' -Skip:(!$IsWindows){ @@ -58,7 +58,7 @@ Describe 'WindowsPowerShell adapter resource tests - requires elevated permissio $r = '{"GetScript": "@{result = $(Get-Content ' + $testFile.replace('\','\\') + ')}", "SetScript": "throw", "TestScript": "throw"}' | dsc resource get -r 'PSDesiredStateConfiguration/Script' -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.actualState.result.properties.result | Should -Be 'test' + $res.actualState.result | Should -Be 'test' } It 'Get works on config with File resource for WinPS' -Skip:(!$IsWindows){ diff --git a/powershell-adapter/powershell.dsc.resource.json b/powershell-adapter/powershell.dsc.resource.json index 76cb1a12..b506c197 100644 --- a/powershell-adapter/powershell.dsc.resource.json +++ b/powershell-adapter/powershell.dsc.resource.json @@ -41,8 +41,7 @@ "$Input | ./psDscAdapter/powershell.resource.ps1 Set" ], "input": "stdin", - "implementsPretest": true, - "return": "state" + "implementsPretest": true }, "test": { "executable": "pwsh", diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index f0ff3090..54cf101a 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -153,7 +153,7 @@ switch ($Operation) { # check if all the desired modules are in the cache $moduleInput | ForEach-Object { if ($dscResourceCache.type -notcontains $_) { - ('DSC resource {0} module not found.' -f $_) | Write-DscTrace -Operation Error + ("DSC resource '{0}' module not found." -f $_) | Write-DscTrace -Operation Error exit 1 } } @@ -189,7 +189,7 @@ switch ($Operation) { @{ valid = $true } | ConvertTo-Json } Default { - Write-Error 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' + Write-DscTrace -Operation Error -Message 'Unsupported operation. Please use one of the following: List, Get, Set, Test, Export, Validate' } } diff --git a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 index da05c4e0..30c3500e 100644 --- a/powershell-adapter/psDscAdapter/psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/psDscAdapter.psm1 @@ -362,33 +362,14 @@ function Get-DscResourceObject { $inputObj = $jsonInput | ConvertFrom-Json $desiredState = [System.Collections.Generic.List[Object]]::new() - # catch potential for improperly formatted configuration input - if ($inputObj.resources -and -not $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') { - 'The input has a top level property named "resources" but is not a configuration. If the input should be a configuration, include the property: "metadata": {"Microsoft.DSC": {"context": "Configuration"}}' | Write-DscTrace -Operation Warn - } - - $adapterName = 'Microsoft.DSC/PowerShell' - - if ($null -ne $inputObj.metadata -and $null -ne $inputObj.metadata.'Microsoft.DSC' -and $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') { - # change the type from pscustomobject to dscResourceObject - $inputObj.resources | ForEach-Object -Process { - $desiredState += [dscResourceObject]@{ - name = $_.name - type = $_.type - properties = $_.properties - } - } - } - else { - # mimic a config object with a single resource - $type = $inputObj.adapted_dsc_type - $inputObj.psobject.properties.Remove('adapted_dsc_type') + $inputObj.resources | ForEach-Object -Process { $desiredState += [dscResourceObject]@{ - name = $adapterName - type = $type - properties = $inputObj + name = $_.name + type = $_.type + properties = $_.properties } } + return $desiredState } @@ -506,6 +487,7 @@ function Invoke-DscOperation { } } + "Output: $($addToActualState | ConvertTo-Json -Depth 10 -Compress)" | Write-DscTrace -Operation Trace return $addToActualState } else { @@ -540,7 +522,7 @@ class dscResourceCache { [dscResourceCacheEntry[]] $ResourceCache } -# format expected for configuration and resource output +# format expected for configuration output class dscResourceObject { [string] $name [string] $type diff --git a/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 index e90b55f0..d28d74a8 100644 --- a/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 @@ -231,7 +231,7 @@ function Invoke-DscCacheRefresh { # fill in resource files (and their last-write-times) that will be used for up-do-date checks $lastWriteTimes = @{} - Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*psm1", "*.mof" -ea Ignore | % { + Get-ChildItem -Recurse -File -Path $dscResource.ParentPath -Include "*.ps1", "*.psd1", "*.psm1", "*.mof" -ea Ignore | % { $lastWriteTimes.Add($_.FullName, $_.LastWriteTime) } @@ -268,11 +268,6 @@ function Get-DscResourceObject { $inputObj = $jsonInput | ConvertFrom-Json $desiredState = [System.Collections.Generic.List[Object]]::new() - # catch potential for improperly formatted configuration input - if ($inputObj.resources -and -not $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') { - 'The input has a top level property named "resources" but is not a configuration. If the input should be a configuration, include the property: "metadata": {"Microsoft.DSC": {"context": "Configuration"}}' | Write-DscTrace -Operation Warn - } - # match adapter to version of powershell if ($PSVersionTable.PSVersion.Major -le 5) { $adapterName = 'Microsoft.Windows/WindowsPowerShell' @@ -281,26 +276,15 @@ function Get-DscResourceObject { $adapterName = 'Microsoft.DSC/PowerShell' } - if ($null -ne $inputObj.metadata -and $null -ne $inputObj.metadata.'Microsoft.DSC' -and $inputObj.metadata.'Microsoft.DSC'.context -eq 'configuration') { - # change the type from pscustomobject to dscResourceObject - $inputObj.resources | ForEach-Object -Process { - $desiredState += [dscResourceObject]@{ - name = $_.name - type = $_.type - properties = $_.properties - } - } - } - else { - # mimic a config object with a single resource - $type = $inputObj.adapted_dsc_type - $inputObj.psobject.properties.Remove('adapted_dsc_type') + # change the type from pscustomobject to dscResourceObject + $inputObj.resources | ForEach-Object -Process { $desiredState += [dscResourceObject]@{ - name = $adapterName - type = $type - properties = $inputObj + name = $_.name + type = $_.type + properties = $_.properties } } + return $desiredState } @@ -377,7 +361,7 @@ function Invoke-DscOperation { # using the cmdlet the appropriate dsc module, and handle errors try { Write-DscTrace -Operation Debug -Message "Module: $($cachedDscResourceInfo.ModuleName), Name: $($cachedDscResourceInfo.Name), Property: $($property)" - $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property + $invokeResult = Invoke-DscResource -Method $Operation -ModuleName $cachedDscResourceInfo.ModuleName -Name $cachedDscResourceInfo.Name -Property $property -ErrorAction Stop if ($invokeResult.GetType().Name -eq 'Hashtable') { $invokeResult.keys | ForEach-Object -Begin { $ResultProperties = @{} } -Process { $ResultProperties[$_] = $invokeResult.$_ } diff --git a/powershell-adapter/windowspowershell.dsc.resource.json b/powershell-adapter/windowspowershell.dsc.resource.json index 54f91a9d..78100f0c 100644 --- a/powershell-adapter/windowspowershell.dsc.resource.json +++ b/powershell-adapter/windowspowershell.dsc.resource.json @@ -41,8 +41,7 @@ "$Input | ./psDscAdapter/powershell.resource.ps1 Set" ], "input": "stdin", - "preTest": true, - "return": "state" + "preTest": true }, "test": { "executable": "powershell", diff --git a/wmi-adapter/Tests/wmi.tests.ps1 b/wmi-adapter/Tests/wmi.tests.ps1 index abbef659..4b697083 100644 --- a/wmi-adapter/Tests/wmi.tests.ps1 +++ b/wmi-adapter/Tests/wmi.tests.ps1 @@ -40,10 +40,10 @@ Describe 'WMI adapter resource tests' { $r = Get-Content -Raw $configPath | dsc config get -f - $LASTEXITCODE | Should -Be 0 $res = $r | ConvertFrom-Json - $res.results[0].result.actualState[0].LastBootUpTime | Should -BeNullOrEmpty - $res.results[0].result.actualState[0].Caption | Should -Not -BeNullOrEmpty - $res.results[0].result.actualState[0].Version | Should -Not -BeNullOrEmpty - $res.results[0].result.actualState[0].OSArchitecture | Should -Not -BeNullOrEmpty + $res.results[0].result.actualState.result[0].properties.LastBootUpTime | Should -BeNullOrEmpty + $res.results[0].result.actualState.result[0].properties.Caption | Should -Not -BeNullOrEmpty + $res.results[0].result.actualState.result[0].properties.Version | Should -Not -BeNullOrEmpty + $res.results[0].result.actualState.result[0].properties.OSArchitecture | Should -Not -BeNullOrEmpty } It 'Example config works' -Skip:(!$IsWindows) { @@ -52,10 +52,10 @@ Describe 'WMI adapter resource tests' { $LASTEXITCODE | Should -Be 0 $r | Should -Not -BeNullOrEmpty $res = $r | ConvertFrom-Json - $res.results[1].result.actualState[0].Name | Should -Not -BeNullOrEmpty - $res.results[1].result.actualState[0].BootupState | Should -BeNullOrEmpty - $res.results[1].result.actualState[1].Caption | Should -Not -BeNullOrEmpty - $res.results[1].result.actualState[1].BuildNumber | Should -BeNullOrEmpty - $res.results[1].result.actualState[4].AdapterType | Should -BeLike "Ethernet*" + $res.results[1].result.actualState.result[0].properties.Name | Should -Not -BeNullOrEmpty + $res.results[1].result.actualState.result[0].properties.BootupState | Should -BeNullOrEmpty + $res.results[1].result.actualState.result[1].properties.Caption | Should -Not -BeNullOrEmpty + $res.results[1].result.actualState.result[1].properties.BuildNumber | Should -BeNullOrEmpty + $res.results[1].result.actualState.result[4].properties.AdapterType | Should -BeLike "Ethernet*" } } diff --git a/wmi-adapter/wmi.resource.ps1 b/wmi-adapter/wmi.resource.ps1 index 183661d3..125d4119 100644 --- a/wmi-adapter/wmi.resource.ps1 +++ b/wmi-adapter/wmi.resource.ps1 @@ -32,14 +32,6 @@ function Write-Trace { $host.ui.WriteErrorLine($trace) } -function IsConfiguration($obj) { - if ($null -ne $obj.metadata -and $null -ne $obj.metadata.'Microsoft.DSC' -and $obj.metadata.'Microsoft.DSC'.context -eq 'configuration') { - return $true - } - - return $false -} - if ($Operation -eq 'List') { $clases = Get-CimClass @@ -89,114 +81,83 @@ elseif ($Operation -eq 'Get') $result = @() - if (IsConfiguration $inputobj_pscustomobj) # we are processing a config batch + foreach ($r in $inputobj_pscustomobj.resources) { - foreach($r in $inputobj_pscustomobj.resources) - { - $type_fields = $r.type -split "/" - $wmi_namespace = $type_fields[0].Replace('.','\') - $wmi_classname = $type_fields[1] + $type_fields = $r.type -split "/" + $wmi_namespace = $type_fields[0].Replace('.','\') + $wmi_classname = $type_fields[1] - # TODO: identify key properties and add WHERE clause to the query - if ($r.properties) + # TODO: identify key properties and add WHERE clause to the query + if ($r.properties) + { + $query = "SELECT $($r.properties.psobject.properties.name -join ',') FROM $wmi_classname" + $where = " WHERE " + $useWhere = $false + $first = $true + foreach ($property in $r.properties.psobject.properties) { - $query = "SELECT $($r.properties.psobject.properties.name -join ',') FROM $wmi_classname" - $where = " WHERE " - $useWhere = $false - $first = $true - foreach ($property in $r.properties.psobject.properties) + # TODO: validate property against the CIM class to give better error message + if ($null -ne $property.value) { - # TODO: validate property against the CIM class to give better error message - if ($null -ne $property.value) + $useWhere = $true + if ($first) { - $useWhere = $true - if ($first) - { - $first = $false - } - else - { - $where += " AND " - } - - if ($property.TypeNameOfValue -eq "System.String") - { - $where += "$($property.Name) = '$($property.Value)'" - } - else - { - $where += "$($property.Name) = $($property.Value)" - } + $first = $false + } + else + { + $where += " AND " } - } - if ($useWhere) - { - $query += $where - } - Write-Trace -Level Trace -message "Query: $query" - $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -Query $query -ErrorAction Stop - } - else - { - $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname -ErrorAction Stop - } - if ($wmi_instances) - { - $instance_result = @{} - # TODO: for a `Get`, they key property must be provided so a specific instance is returned rather than just the first - $wmi_instance = $wmi_instances[0] # for 'Get' we return just first matching instance; for 'export' we return all instances - $wmi_instance.psobject.properties | %{ - if (($_.Name -ne "type") -and (-not $_.Name.StartsWith("Cim"))) + if ($property.TypeNameOfValue -eq "System.String") { - if ($r.properties) - { - if ($r.properties.psobject.properties.name -contains $_.Name) - { - $instance_result[$_.Name] = $_.Value - } - } - else - { - $instance_result[$_.Name] = $_.Value - } + $where += "$($property.Name) = '$($property.Value)'" + } + else + { + $where += "$($property.Name) = $($property.Value)" } } - - $result += @($instance_result) } + if ($useWhere) + { + $query += $where + } + Write-Trace -Level Trace -message "Query: $query" + $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -Query $query -ErrorAction Stop + } + else + { + $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname -ErrorAction Stop } - } - else # we are processing an individual resource call - { - $type_fields = $inputobj_pscustomobj.adapted_dsc_type -split "/" - $wmi_namespace = $type_fields[0].Replace('.','\') - $wmi_classname = $type_fields[1] - - #TODO: add filtering based on supplied properties of $inputobj_pscustomobj - $wmi_instances = Get-CimInstance -Namespace $wmi_namespace -ClassName $wmi_classname -ErrorAction Stop if ($wmi_instances) { - # TODO: there's duplicate code here between configuration and non-configuration execution and should be refactored into a helper - $wmi_instance = $wmi_instances[0] - $result = @{} + $instance_result = [ordered]@{} + # TODO: for a `Get`, they key property must be provided so a specific instance is returned rather than just the first + $wmi_instance = $wmi_instances[0] # for 'Get' we return just first matching instance; for 'export' we return all instances $wmi_instance.psobject.properties | %{ if (($_.Name -ne "type") -and (-not $_.Name.StartsWith("Cim"))) { - $result[$_.Name] = $_.Value + if ($r.properties) + { + if ($r.properties.psobject.properties.name -contains $_.Name) + { + $instance_result[$_.Name] = $_.Value + } + } + else + { + $instance_result[$_.Name] = $_.Value + } } } - } - else - { - $errmsg = "Can not find type " + $inputobj_pscustomobj.type + "; please ensure that Get-CimInstance returns this resource type" - Write-Trace $errmsg - exit 1 + + $result += [pscustomobject]@{ name = $r.name; type = $r.type; properties = $instance_result } } } - $result | ConvertTo-Json -Compress + @{result = $result } | ConvertTo-Json -Depth 10 -Compress } elseif ($Operation -eq 'Validate') {