diff --git a/Cargo.lock b/Cargo.lock index e64487eee..9fb54391a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -914,7 +914,7 @@ dependencies = [ [[package]] name = "dsc-resource-registry" -version = "1.0.0" +version = "1.1.0" dependencies = [ "clap", "crossterm", diff --git a/lib/dsc-lib-jsonschema/.versions.json b/lib/dsc-lib-jsonschema/.versions.json index 6ffb355c8..ccb9af183 100644 --- a/lib/dsc-lib-jsonschema/.versions.json +++ b/lib/dsc-lib-jsonschema/.versions.json @@ -1,10 +1,11 @@ { "latestMajor": "V3", "latestMinor": "V3_2", - "latestPatch": "V3_2_0", + "latestPatch": "V3_2_1", "all": [ "V3", "V3_2", + "V3_2_1", "V3_2_0", "V3_1", "V3_1_3", diff --git a/lib/dsc-lib/locales/en-us.toml b/lib/dsc-lib/locales/en-us.toml index 81083031c..90ed80f09 100644 --- a/lib/dsc-lib/locales/en-us.toml +++ b/lib/dsc-lib/locales/en-us.toml @@ -192,6 +192,8 @@ exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' runProcessError = "Failed to run process '%{executable}': %{error}" whatIfWarning = "Resource '%{resource}' uses deprecated 'whatIf' operation. See https://github.com/PowerShell/DSC/issues/1361 for migration information." securityContextRequired = "Operation '%{operation}' for resource '%{resource}' requires security context '%{context}'" +noAdaptedContent = "No adapted content available for resource '%{resource}'" +invalidAdaptedContent = "Invalid adapted content for resource '%{resource}': %{error}" [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 4da974bc7..afbc9084e 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -4,6 +4,7 @@ use crate::{discovery::{DiscoveryExtensionCache, DiscoveryManifestCache, DiscoveryResourceCache, discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, matches_adapter_requirement}, dscresources::{adapted_resource_manifest::AdaptedDscResourceManifest, resource_manifest::SetDeleteArgKind}, parser::Statement, types::{FullyQualifiedTypeName, TypeNameFilter}}; use crate::{locked_clear, locked_is_empty, locked_extend, locked_clone, locked_get}; use crate::configure::{config_doc::ResourceDiscoveryMode, context::Context}; +use crate::dscresources::adapted_resource_manifest::AdaptedPathOrContent; use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs}; use crate::dscresources::resource_manifest::{Kind, ResourceManifest, SchemaKind}; use crate::dscresources::command_resource::invoke_command; @@ -31,7 +32,6 @@ const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extens const DSC_MANIFEST_LIST_EXTENSIONS: [&str; 3] = [".dsc.manifests.json", ".dsc.manifests.yaml", ".dsc.manifests.yml"]; const DSC_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.resource.json", ".dsc.resource.yaml", ".dsc.resource.yml"]; -// use BTreeMap so that the results are sorted by the typename, the Vec is sorted by version static ADAPTERS: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryResourceCache::new())); static RESOURCES: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryResourceCache::new())); static EXTENSIONS: LazyLock> = LazyLock::new(|| RwLock::new(DiscoveryExtensionCache::new())); @@ -391,38 +391,39 @@ impl ResourceDiscovery for CommandDiscovery { let mut adapter_resources_count = 0; // invoke the list command - let list_command = &manifest.adapter.clone().unwrap().list; - let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args.clone(), None, Some(&adapter.directory), None, manifest.exit_codes.as_ref()) - { - Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), - Err(e) => { - // In case of error, log and continue - warn!("{e}"); - continue; - }, - }; + if let Some(list_command) = &manifest.adapter.clone().unwrap().list { + let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args.clone(), None, Some(&adapter.directory), None, manifest.exit_codes.as_ref()) + { + Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr), + Err(e) => { + // In case of error, log and continue + warn!("{e}"); + continue; + }, + }; - if exit_code != 0 { - // in case of failure, log and continue - warn!("Adapter failed to list resources with exit code {exit_code}: {stderr}"); - continue; - } + if exit_code != 0 { + // in case of failure, log and continue + warn!("Adapter failed to list resources with exit code {exit_code}: {stderr}"); + continue; + } - for line in stdout.lines() { - match serde_json::from_str::(line){ - Result::Ok(resource) => { - if resource.require_adapter.is_none() { - warn!("{}", DscError::MissingRequires(adapter_name.to_string(), resource.type_name.to_string()).to_string()); - continue; - } + for line in stdout.lines() { + match serde_json::from_str::(line){ + Result::Ok(resource) => { + if resource.require_adapter.is_none() { + warn!("{}", DscError::MissingRequires(adapter_name.to_string(), resource.type_name.to_string()).to_string()); + continue; + } - if name_filter.is_match(&resource.type_name) { - insert_resource(&mut adapted_resources, &resource); - adapter_resources_count += 1; + if name_filter.is_match(&resource.type_name) { + insert_resource(&mut adapted_resources, &resource); + adapter_resources_count += 1; + } + }, + Result::Err(err) => { + warn!("Failed to parse resource: {line} -> {err}"); } - }, - Result::Err(err) => { - warn!("Failed to parse resource: {line} -> {err}"); } } } @@ -748,13 +749,22 @@ fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceMani )); } + let mut resource = DscResource::new(); let directory = path.parent().unwrap(); - let resource_path = directory.join(&manifest.path); - if !resource_path.exists() { - return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = manifest.type_name).to_string())); + match &manifest.path_or_content { + AdaptedPathOrContent::Path(resource_path) => { + let resource_path = directory.join(resource_path); + if !resource_path.exists() { + return Err(DscError::InvalidManifest(t!("discovery.commandDiscovery.adaptedResourcePathNotFound", path = resource_path.to_string_lossy(), resource = manifest.type_name).to_string())); + } + resource.path = resource_path; + }, + AdaptedPathOrContent::Content(content) => { + resource.path = path.to_path_buf(); + resource.adapted_content = Some(content.clone()); + } } - let mut resource = DscResource::new(); resource.type_name = manifest.type_name.clone(); resource.kind = Kind::Resource; resource.implemented_as = None; @@ -763,7 +773,6 @@ fn load_adapted_resource_manifest(path: &Path, manifest: &AdaptedDscResourceMani resource.version = manifest.version.clone(); resource.capabilities = manifest.capabilities.clone(); resource.require_adapter = Some(manifest.require_adapter.clone()); - resource.path = resource_path; resource.directory = directory.to_path_buf(); resource.manifest = None; resource.schema = Some(manifest.schema.clone()); diff --git a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs index 906c3af86..e13d38db7 100644 --- a/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/adapted_resource_manifest.rs @@ -14,6 +14,13 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use std::path::PathBuf; +#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub enum AdaptedPathOrContent { + Path(PathBuf), + Content(Map), +} + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema, DscRepoSchema)] #[serde(deny_unknown_fields, rename_all = "camelCase")] #[dsc_repo_schema( @@ -45,8 +52,9 @@ pub struct AdaptedDscResourceManifest { /// An optional message indicating the resource is deprecated. If provided, the message will be shown when the resource is used. #[serde(skip_serializing_if = "Option::is_none")] pub deprecation_message: Option, - /// The file path to the resource. - pub path: PathBuf, + /// The file path to the resource or the content of the resource itself. + #[serde(flatten)] + pub path_or_content: AdaptedPathOrContent, /// The description of the resource. pub description: Option, /// The author of the resource. diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index c03525ab6..59f1e6f43 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -7,8 +7,8 @@ use jsonschema::Validator; use rust_i18n::t; use serde::Deserialize; use serde_json::{Map, Value}; -use std::{collections::HashMap, env, path::{Path, PathBuf}, process::Stdio}; -use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::resource_manifest::SchemaArgKind, types::{ExitCodesMap, FullyQualifiedTypeName}, util::canonicalize_which}; +use std::{collections::HashMap, env, path::Path, process::Stdio}; +use crate::{configure::{config_doc::{ExecutionKind, SecurityContextKind}, config_result::{ResourceGetResult, ResourceTestResult}}, dscresources::{resource_manifest::SchemaArgKind}, types::ExitCodesMap, util::canonicalize_which}; use crate::dscerror::DscError; use super::{ dscresource::{get_diff, redact, DscResource}, @@ -26,11 +26,6 @@ use tokio::{io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, process::Command}; pub const EXIT_PROCESS_TERMINATED: i32 = 0x102; -pub struct CommandResourceInfo { - pub type_name: FullyQualifiedTypeName, - pub path: Option, -} - /// Invoke the get operation on a resource /// /// # Arguments @@ -50,17 +45,12 @@ pub fn invoke_get(resource: &DscResource, filter: &str, target_resource: Option< let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), + let command_resource = match target_resource { + Some(target) => target, + None => resource }; - validate_security_context(&get.require_security_context, &resource_type, "get")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, - }; - let args = process_get_args(get.args.as_ref(), filter, &command_resource_info); + validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?; + let args = process_get_args(get.args.as_ref(), filter, command_resource); if !filter.is_empty() { verify_json_from_manifest(resource, filter, target_resource)?; command_input = get_command_input(get.input.as_ref(), filter)?; @@ -110,16 +100,10 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }; let operation_type: String; let mut is_synthetic_what_if = false; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(target) => target, + None => resource }; - let set_method = match execution_type { ExecutionKind::Actual => { operation_type = "set".to_string(); @@ -132,7 +116,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let (_, supports_whatif) = process_set_delete_args( set.args.as_ref(), "", - &command_resource_info, + command_resource, execution_type ); supports_whatif @@ -141,7 +125,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut if has_native_whatif { &manifest.set } else if manifest.what_if.is_some() { - warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &resource_type)); + warn!("{}", t!("dscresources.commandResource.whatIfWarning", resource = &command_resource.type_name)); &manifest.what_if } else { is_synthetic_what_if = true; @@ -152,12 +136,12 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let Some(set) = set_method.as_ref() else { return Err(DscError::NotImplemented("set".to_string())); }; - validate_security_context(&set.require_security_context, &resource_type, "set")?; + validate_security_context(&set.require_security_context, &command_resource.type_name, "set")?; verify_json_from_manifest(resource, desired, target_resource)?; // 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.type_name)); + info!("{}", t!("dscresources.commandResource.noPretest", resource = &command_resource.type_name)); let test_result = invoke_test(resource, desired, target_resource)?; if is_synthetic_what_if { return Ok(test_result.into()); @@ -193,20 +177,15 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let Some(get) = &manifest.get else { return Err(DscError::NotImplemented("get".to_string())); }; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - validate_security_context(&get.require_security_context, &resource_type, "get")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; - let args = process_get_args(get.args.as_ref(), desired, &command_resource_info); + validate_security_context(&get.require_security_context, &command_resource.type_name, "get")?; + let args = process_get_args(get.args.as_ref(), desired, command_resource); let command_input = get_command_input(get.input.as_ref(), desired)?; - info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &resource.type_name, executable = &get.executable)); + info!("{}", t!("dscresources.commandResource.setGetCurrent", resource = &command_resource.type_name, executable = &get.executable)); let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; if resource.kind == Kind::Resource { @@ -235,7 +214,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut let mut env: Option> = None; let mut input_desired: Option<&str> = None; - let (args, _) = process_set_delete_args(set.args.as_ref(), desired, &command_resource_info, execution_type); + let (args, _) = process_set_delete_args(set.args.as_ref(), desired, command_resource, execution_type); match &set.input { Some(InputKind::Env) => { env = Some(json_to_hashmap(desired)?); @@ -260,7 +239,7 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut Some(ReturnKind::State) => { if resource.kind == Kind::Resource { - debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &resource.type_name, executable = &set.executable)); + debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &command_resource.type_name, executable = &set.executable)); verify_json_from_manifest(resource, &stdout, target_resource)?; } @@ -288,14 +267,14 @@ pub fn invoke_set(resource: &DscResource, desired: &str, skip_test: bool, execut }; if resource.kind == Kind::Resource { - debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &resource.type_name, executable = &set.executable)); + debug!("{}", t!("dscresources.commandResource.setVerifyOutput", operation = operation_type, resource = &command_resource.type_name, executable = &set.executable)); verify_json_from_manifest(resource, actual_line, target_resource)?; } let actual_value: Value = serde_json::from_str(actual_line)?; // TODO: need schema for diff_properties to validate against let Some(diff_line) = lines.next() else { - return Err(DscError::Command(resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); + return Err(DscError::Command(command_resource.type_name.to_string(), exit_code, t!("dscresources.commandResource.setUnexpectedDiff").to_string())); }; let diff_properties: Vec = serde_json::from_str(diff_line)?; Ok(SetResult::Resource(ResourceSetResponse { @@ -353,23 +332,18 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti verify_json_from_manifest(resource, expected, target_resource)?; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; - validate_security_context(&test.require_security_context, &resource_type, "test")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, - }; - let args = process_get_args(test.args.as_ref(), expected, &command_resource_info); + validate_security_context(&test.require_security_context, &command_resource.type_name, "test")?; + let args = process_get_args(test.args.as_ref(), expected, command_resource); let command_input = get_command_input(test.input.as_ref(), expected)?; - info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &resource.type_name, executable = &test.executable)); + info!("{}", t!("dscresources.commandResource.invokeTestUsing", resource = &command_resource.type_name, executable = &test.executable)); let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; - if resource.kind == Kind::Importer { + if command_resource.kind == Kind::Importer { debug!("{}", t!("dscresources.commandResource.testGroupTestResponse")); let group_test_response: Vec = serde_json::from_str(&stdout)?; return Ok(TestResult::Group(group_test_response)); @@ -378,8 +352,8 @@ pub fn invoke_test(resource: &DscResource, expected: &str, target_resource: Opti let mut expected_value: Value = serde_json::from_str(expected)?; match test.returns { Some(ReturnKind::State) => { - if resource.kind == Kind::Resource { - debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &resource.type_name, executable = &test.executable)); + if command_resource.kind == Kind::Resource { + debug!("{}", t!("dscresources.commandResource.testVerifyOutput", resource = &command_resource.type_name, executable = &test.executable)); verify_json_from_manifest(resource, &stdout, target_resource)?; } @@ -510,17 +484,12 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti verify_json_from_manifest(resource, filter, target_resource)?; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - validate_security_context(&delete.require_security_context, &resource_type, "delete")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; - let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, &command_resource_info, execution_type); + validate_security_context(&delete.require_security_context, &command_resource.type_name, "delete")?; + let (args, supports_whatif) = process_set_delete_args(delete.args.as_ref(), filter, command_resource, execution_type); if execution_type == &ExecutionKind::WhatIf && !supports_whatif { // perform a synthetic what-if by calling test and wrapping the TestResult in DeleteResultKind::SyntheticWhatIf let test_result = invoke_test(resource, filter, target_resource)?; @@ -528,7 +497,7 @@ pub fn invoke_delete(resource: &DscResource, filter: &str, target_resource: Opti } let command_input = get_command_input(delete.input.as_ref(), filter)?; - info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = resource_type, executable = &delete.executable)); + info!("{}", t!("dscresources.commandResource.invokeDeleteUsing", resource = &command_resource.type_name, executable = &delete.executable)); let (_exit_code, stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result = if execution_type == &ExecutionKind::WhatIf { let delete_result: DeleteResult = serde_json::from_str(&stdout)?; @@ -564,19 +533,14 @@ pub fn invoke_validate(resource: &DscResource, config: &str, target_resource: Op return Err(DscError::NotImplemented("validate".to_string())); }; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), + let target_resource = match target_resource { + Some(target) => target, + None => resource }; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, - }; - let args = process_get_args(validate.args.as_ref(), config, &command_resource_info); + let args = process_get_args(validate.args.as_ref(), config, target_resource); let command_input = get_command_input(validate.input.as_ref(), config)?; - info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = resource_type, executable = &validate.executable)); + info!("{}", t!("dscresources.commandResource.invokeValidateUsing", resource = &resource.type_name, executable = &validate.executable)); let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; let result: ValidateResult = serde_json::from_str(&stdout)?; Ok(result) @@ -595,22 +559,28 @@ pub fn get_schema(resource: &DscResource, target_resource: Option<&DscResource>) let Some(manifest) = &resource.manifest else { return Err(DscError::MissingManifest(resource.type_name.to_string())); }; + + let target_resource = match target_resource { + Some(r) => r, + None => resource, + }; + + if let Some(schema) = &target_resource.schema { + return Ok(serde_json::to_string(schema)?); + } + let Some(schema_kind) = manifest.schema.as_ref() else { - return Err(DscError::SchemaNotAvailable(resource.type_name.to_string())); + return Err(DscError::SchemaNotAvailable(target_resource.type_name.to_string())); }; match schema_kind { SchemaKind::Command(command) => { - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - let args = process_schema_args(command.args.as_ref(), &CommandResourceInfo { type_name: resource_type, path: None }); + let args = process_schema_args(command.args.as_ref(), target_resource); let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, args, None, Some(&resource.directory), None, manifest.exit_codes.as_ref())?; Ok(stdout) }, SchemaKind::Embedded(schema) => { - let json = serde_json::to_string(schema)?; + let json = serde_json::to_string(&schema)?; Ok(json) }, } @@ -661,16 +631,11 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc let mut command_input: CommandInput = CommandInput { env: None, stdin: None }; let args: Option>; - let resource_type = match target_resource { - Some(r) => r.type_name.clone(), - None => resource.type_name.clone(), - }; - validate_security_context(&export.require_security_context, &resource_type, "export")?; - let path = target_resource.map(|target_resource| target_resource.path.clone()); - let command_resource_info = CommandResourceInfo { - type_name: resource_type.clone(), - path, + let command_resource = match target_resource { + Some(r) => r, + None => resource, }; + validate_security_context(&export.require_security_context, &command_resource.type_name, "export")?; if let Some(input) = input { if !input.is_empty() { verify_json_from_manifest(resource, input, target_resource)?; @@ -678,9 +643,9 @@ pub fn invoke_export(resource: &DscResource, input: Option<&str>, target_resourc command_input = get_command_input(export.input.as_ref(), input)?; } - args = process_get_args(export.args.as_ref(), input, &command_resource_info); + args = process_get_args(export.args.as_ref(), input, command_resource); } else { - args = process_get_args(export.args.as_ref(), "", &command_resource_info); + args = process_get_args(export.args.as_ref(), "", command_resource); } let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(&resource.directory), command_input.env, manifest.exit_codes.as_ref())?; @@ -728,12 +693,10 @@ pub fn invoke_resolve(resource: &DscResource, input: &str) -> Result>, input: Option /// # Returns /// /// A vector of strings representing the processed arguments -pub fn process_get_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo) -> Option> { +pub fn process_get_args(args: Option<&Vec>, input: &str, resource: &DscResource) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -951,6 +914,21 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, command_res GetArgKind::String(s) => { processed_args.push(s.clone()); }, + GetArgKind::AdaptedContent { adapted_content_arg } => { + processed_args.push(adapted_content_arg.clone()); + if let Some(adapted_content) = &resource.adapted_content { + match serde_json::to_string(&adapted_content) { + Err(e) => { + warn!("{}", t!("dscresources.commandResource.invalidAdaptedContent", resource = &resource.type_name, error = e)); + } + Ok(s) => { + processed_args.push(s); + } + } + } else { + debug!("{}", t!("dscresources.commandResource.noAdaptedContent", resource = &resource.type_name)); + } + }, GetArgKind::Json { json_input_arg, mandatory } => { if input.is_empty() && *mandatory != Some(true) { continue; @@ -961,16 +939,14 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, command_res }, GetArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); - processed_args.push(command_resource_info.type_name.to_string()); + processed_args.push(resource.type_name.to_string()); }, GetArgKind::ResourcePath { resource_path_arg, include_quotes} => { - if let Some(path) = &command_resource_info.path { - processed_args.push(resource_path_arg.clone()); - if *include_quotes { - processed_args.push(format!("\"{}\"", path.to_string_lossy())); - } else { - processed_args.push(path.to_string_lossy().to_string()); - } + processed_args.push(resource_path_arg.clone()); + if *include_quotes { + processed_args.push(format!("\"{}\"", resource.path.to_string_lossy())); + } else { + processed_args.push(resource.path.to_string_lossy().to_string()); } }, } @@ -979,7 +955,7 @@ pub fn process_get_args(args: Option<&Vec>, input: &str, command_res Some(processed_args) } -fn process_schema_args(args: Option<&Vec>, command_resource_info: &CommandResourceInfo) -> Option> { +fn process_schema_args(args: Option<&Vec>, command_resource: &DscResource) -> Option> { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return None; @@ -993,7 +969,7 @@ fn process_schema_args(args: Option<&Vec>, command_resource_info: }, SchemaArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); - processed_args.push(command_resource_info.type_name.to_string()); + processed_args.push(command_resource.type_name.to_string()); }, } } @@ -1011,7 +987,7 @@ fn process_schema_args(args: Option<&Vec>, command_resource_info: /// # Returns /// /// A vector of strings representing the processed arguments -fn process_set_delete_args(args: Option<&Vec>, input: &str, command_resource_info: &CommandResourceInfo, execution_type: &ExecutionKind) -> (Option>, bool) { +fn process_set_delete_args(args: Option<&Vec>, input: &str, resource: &DscResource, execution_type: &ExecutionKind) -> (Option>, bool) { let Some(arg_values) = args else { debug!("{}", t!("dscresources.commandResource.noArgs")); return (None, false); @@ -1024,6 +1000,14 @@ fn process_set_delete_args(args: Option<&Vec>, input: &str, co SetDeleteArgKind::String(s) => { processed_args.push(s.clone()); }, + SetDeleteArgKind::AdaptedContent { adapted_content_arg } => { + processed_args.push(adapted_content_arg.clone()); + if let Some(adapted_content) = &resource.adapted_content { + processed_args.push(serde_json::to_string(&adapted_content).unwrap()); + } else { + debug!("{}", t!("dscresources.commandResource.noAdaptedContent", resource = &resource.type_name)); + } + }, SetDeleteArgKind::Json { json_input_arg, mandatory } => { if input.is_empty() && *mandatory != Some(true) { continue; @@ -1033,18 +1017,16 @@ fn process_set_delete_args(args: Option<&Vec>, input: &str, co processed_args.push(input.to_string()); }, SetDeleteArgKind::ResourcePath { resource_path_arg, include_quotes} => { - if let Some(path) = &command_resource_info.path { - processed_args.push(resource_path_arg.clone()); - if *include_quotes { - processed_args.push(format!("\"{}\"", path.to_string_lossy())); - } else { - processed_args.push(path.to_string_lossy().to_string()); - } + processed_args.push(resource_path_arg.clone()); + if *include_quotes { + processed_args.push(format!("\"{}\"", resource.path.to_string_lossy())); + } else { + processed_args.push(resource.path.to_string_lossy().to_string()); } }, SetDeleteArgKind::ResourceType { resource_type_arg } => { processed_args.push(resource_type_arg.clone()); - processed_args.push(command_resource_info.type_name.to_string()); + processed_args.push(resource.type_name.to_string()); }, SetDeleteArgKind::WhatIf { what_if_arg } => { supports_whatif = true; diff --git a/lib/dsc-lib/src/dscresources/dscresource.rs b/lib/dsc-lib/src/dscresources/dscresource.rs index 6b5d544e3..f3f57c0b9 100644 --- a/lib/dsc-lib/src/dscresources/dscresource.rs +++ b/lib/dsc-lib/src/dscresources/dscresource.rs @@ -61,6 +61,8 @@ pub struct DscResource { pub target_resource: Option>, /// The manifest of the resource. pub manifest: Option, + /// The content of the adapted resource, if available. + pub adapted_content: Option>, } #[derive(Clone, Debug, Eq, Hash, PartialEq, Deserialize, Serialize, JsonSchema, DscRepoSchema, Ord, PartialOrd)] @@ -116,6 +118,7 @@ impl DscResource { schema: None, target_resource: None, manifest: None, + adapted_content: None, } } diff --git a/lib/dsc-lib/src/dscresources/resource_manifest.rs b/lib/dsc-lib/src/dscresources/resource_manifest.rs index 568e44f9b..93d558712 100644 --- a/lib/dsc-lib/src/dscresources/resource_manifest.rs +++ b/lib/dsc-lib/src/dscresources/resource_manifest.rs @@ -104,7 +104,11 @@ pub struct ResourceManifest { pub enum GetArgKind { /// The argument is a string. String(String), - /// The argument accepts the JSON input object. + AdaptedContent { + /// The argument that accepts the JSON content from the manifest. + #[serde(rename = "adaptedContentArg")] + adapted_content_arg: String, + }, Json { /// The argument that accepts the JSON input object. #[serde(rename = "jsonInputArg")] @@ -133,7 +137,11 @@ pub enum GetArgKind { pub enum SetDeleteArgKind { /// The argument is a string. String(String), - /// The argument accepts the JSON input object. + AdaptedContent { + /// The argument that accepts the JSON content from the manifest. + #[serde(rename = "adaptedContentArg")] + adapted_content_arg: String, + }, Json { /// The argument that accepts the JSON input object. #[serde(rename = "jsonInputArg")] @@ -330,7 +338,7 @@ pub struct ResolveMethod { #[dsc_repo_schema(base_name = "manifest.adapter", folder_path = "resource")] pub struct Adapter { /// The way to list adapter supported resources. - pub list: ListMethod, + pub list: Option, /// Defines how the adapter supports accepting configuration. #[serde(alias = "config", rename = "inputKind")] pub input_kind: AdapterInputKind, diff --git a/lib/dsc-lib/src/extensions/discover.rs b/lib/dsc-lib/src/extensions/discover.rs index 4b017ae3f..488703590 100644 --- a/lib/dsc-lib/src/extensions/discover.rs +++ b/lib/dsc-lib/src/extensions/discover.rs @@ -8,7 +8,7 @@ use crate::{ dscerror::DscError, dscresources::{ command_resource::{ - invoke_command, process_get_args, CommandResourceInfo + invoke_command, process_get_args, }, dscresource::DscResource, resource_manifest::GetArgKind, @@ -68,11 +68,10 @@ impl DscExtension { let Some(discover) = extension.discover else { return Err(DscError::UnsupportedCapability(self.type_name.to_string(), Capability::Discover.to_string())); }; - let command_resource_info = CommandResourceInfo { - type_name: self.type_name.clone(), - path: None, - }; - let args = process_get_args(discover.args.as_ref(), "", &command_resource_info); + let mut extension_resource = DscResource::new(); + extension_resource.type_name = self.type_name.clone(); + extension_resource.path = self.path.clone(); + let args = process_get_args(discover.args.as_ref(), "", &extension_resource); if let Some(deprecation_message) = extension.deprecation_message.as_ref() { warn!("{}", t!("extensions.dscextension.deprecationMessage", extension = self.type_name, message = deprecation_message)); } diff --git a/resources/registry/Cargo.toml b/resources/registry/Cargo.toml index 3d89a7b7a..fb06a0ef2 100644 --- a/resources/registry/Cargo.toml +++ b/resources/registry/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dsc-resource-registry" -version = "1.0.0" +version = "1.1.0" edition = "2024" [[bin]] diff --git a/resources/registry/locales/en-us.toml b/resources/registry/locales/en-us.toml index eb2f897af..42c77e96a 100644 --- a/resources/registry/locales/en-us.toml +++ b/resources/registry/locales/en-us.toml @@ -1,7 +1,30 @@ _version = 1 +[adapter] +getProcessingKey = "Processing key: %{key}" +getNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key}" +setProcessingKey = "Setting key: %{key}" +setNoAdaptedRegistryValueFound = "No adapted registry value found for key: %{key}" +registryValueNotFound = "Registry value not found for key path: %{key_path} and value name: %{value_name}" +unsupportedConversionToJsonType = "Unsupported conversion from %{registry_value_data} to JSON type: %{json_type}" +unsupportedValueType = "For value name %{value_name}, unsupported value type: %{value}" +couldNotConvertDefaultValue = "Could not convert default value '%{default_value}' to registry data type: %{reg_type}" +valueMappingNotFound = "No mapping found for value '%{value}' of type %{reg_type}" +mappedValueNotU64 = "Mapped value for '%{value}' is not a u64" +mappedValueNotString = "Mapped value for '%{value}' is not a string" +unmappedByteSlice = "No mapping found for byte slice: %{hex_string}" +nonStringInMultiStringDefault = "Non-string value found in default value array for REG_MULTI_SZ: %{value}" +emptyReverseMap = "The reverse map is empty" +mapJsonToRegistryNotFound = "No mapping found for key: %{key}" + [args] about = "Manage state of Windows registry" +adapterAbout = "Use adapted registry resources." +adapterArgsInputHelp = "The registry JSON input for the adapted resource." +adapterExportAbout = "Use 'export' operation with adapted registry resources." +adapterGetAbout = "Use 'get' operation with adapted registry resources." +adapterSetAbout = "Use 'set' operation with adapted registry resources." +adapterArgsAdaptedResourceHelp = "The content of the adapted resource to use." configAbout = "Manage registry configuration." configArgsInputHelp = "The registry JSON input." configArgsWhatIfHelp = "Run as a what-if operation instead of applying the registry configuration." diff --git a/resources/registry/registry.dsc.manifests.json b/resources/registry/registry.dsc.manifests.json index 260b860d8..a2b86b719 100644 --- a/resources/registry/registry.dsc.manifests.json +++ b/resources/registry/registry.dsc.manifests.json @@ -117,6 +117,68 @@ ] } } + }, + { + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.Windows.Adapter/Registry", + "kind": "adapter", + "description": "Adapter for registry based resources", + "tags": [ + "Windows" + ], + "version": "0.1.0", + "adapter": { + "inputKind": "single" + }, + "get": { + "executable": "registry", + "args": [ + "adapter", + "get", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "adaptedContentArg": "--adapted-resource" + } + ] + }, + "set": { + "executable": "registry", + "args": [ + "adapter", + "set", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "adaptedContentArg": "--adapted-resource" + } + ] + }, + "export": { + "executable": "registry", + "args": [ + "adapter", + "export", + { + "jsonInputArg": "--input", + "mandatory": true + }, + { + "adaptedContentArg": "--adapted-resource" + } + ] + }, + "exitCodes": { + "0": "Success", + "1": "Invalid parameter", + "2": "Invalid input", + "3": "Registry error", + "4": "JSON serialization failed" + } } ] } diff --git a/resources/registry/src/adapter.rs b/resources/registry/src/adapter.rs new file mode 100644 index 000000000..cbef5c582 --- /dev/null +++ b/resources/registry/src/adapter.rs @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::HashMap; + +use crate::error::RegistryResourceError; +use dsc_lib_registry::{RegistryHelper, config::RegistryValueData}; +use rust_i18n::t; +use serde::Deserialize; +use serde_json::{Map, Value}; +use tracing::{debug, trace, warn}; + +#[derive(Deserialize)] +struct AdaptedRegistryResource { + #[serde(flatten)] + properties: Map, +} + +#[derive(Debug, Deserialize)] +enum RegistryDataType { + #[serde(rename = "REG_BINARY")] + Binary, + #[serde(rename = "REG_DWORD")] + Dword, + #[serde(rename = "REG_EXPAND_SZ")] + ExpandString, + #[serde(rename = "REG_MULTI_SZ")] + MultiString, + #[serde(rename = "REG_SZ")] + String, + #[serde(rename = "REG_QWORD")] + Qword, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +enum JsonType { + Boolean, + BooleanArray, + Number, + NumberArray, + String, + StringArray, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct AdaptedRegistryValue { + key_path: String, + value_name: String, + value_type: RegistryDataType, + json_type: JsonType, + map_json_to_registry: Value, + default_value_if_not_found: Value, +} + +fn build_resource_map(adapted_resource: &str) -> Result, RegistryResourceError> { + let adapted: AdaptedRegistryResource = serde_json::from_str(adapted_resource) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + adapted.properties.into_iter() + .map(|(k, v)| { + let arv: AdaptedRegistryValue = serde_json::from_value(v) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + Ok((k, arv)) + }) + .collect() +} + +pub fn adapter_get(input: &str, adapted_resource: &str) -> Result { + let resource_map = build_resource_map(adapted_resource)?; + let mut result = Map::new(); + + let input_map: Map = serde_json::from_str(input) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + for (key, value) in input_map.iter() { + if let Some(adapted_registry_value) = resource_map.get(key) { + debug!("{}", t!("adapter.getProcessingKey", key = key)); + if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { + let registry_data = get_registry_value_data(&adapted_registry_value.value_name, value, json_map, &adapted_registry_value.value_type)?; + let registry_helper = RegistryHelper::new(&adapted_registry_value.key_path, Some(adapted_registry_value.value_name.clone()), Some(registry_data))?; + match registry_helper.get() { + Ok(registry) => { + if let Some(exist) = registry.exist && !exist { + let default_registry_data = convert_default_value_to_registry_data(&adapted_registry_value.default_value_if_not_found, &adapted_registry_value.value_type)?; + let json_value = convert_registry_value_data_to_mapped_json(&default_registry_data, &adapted_registry_value.json_type, json_map)?; + result.insert(key.clone(), json_value); + continue; + } + if let Some(registry_value) = registry.value_data { + let json_value = convert_registry_value_data_to_mapped_json(®istry_value, &adapted_registry_value.json_type, json_map)?; + result.insert(key.clone(), json_value); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.registryValueNotFound", key_path = adapted_registry_value.key_path, value_name = adapted_registry_value.value_name).to_string())); + } + }, + Err(e) => { + return Err(RegistryResourceError::RegistryError(e)); + } + } + } else { + warn!("No mapping found for key {}", key); + } + } else { + debug!("{}", t!("adapter.getNoAdaptedRegistryValueFound", key = key)); + } + } + + serde_json::to_string(&result).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string())) +} + +pub fn adapter_set(input: &str, adapted_resource: &str) -> Result<(), RegistryResourceError> { + let resource_map = build_resource_map(adapted_resource)?; + + let input_map: Map = serde_json::from_str(input) + .map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + for (key, value) in input_map.iter() { + if let Some(adapted_registry_value) = resource_map.get(key) { + debug!("{}", t!("adapter.setProcessingKey", key = key)); + if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { + let registry_data = get_registry_value_data(&adapted_registry_value.value_name, value, json_map, &adapted_registry_value.value_type)?; + let registry_helper = RegistryHelper::new(&adapted_registry_value.key_path, Some(adapted_registry_value.value_name.clone()), Some(registry_data))?; + if let Err(e) = registry_helper.set() { + return Err(RegistryResourceError::RegistryError(e)); + } + } else { + warn!("No mapping found for key {}", key); + } + } else { + debug!("{}", t!("adapter.setNoAdaptedRegistryValueFound", key = key)); + } + } + + Ok(()) +} + +pub fn adapter_export(input: &str, adapted_resource: &str) -> Result { + trace!("Adapter Export with input: {input}"); + + // if input is provided, use that to perform a `get` and return that result + // if no input is provided, then create an input that contains all keys in the adapted resource with first values and perform a `get` to return the default values for all keys + let input: String = if input.is_empty() { + let resource_map = build_resource_map(adapted_resource)?; + let mut map = Map::new(); + for (key, adapted_registry_value) in &resource_map { + if let Some(json_map) = adapted_registry_value.map_json_to_registry.as_object() { + let first_key = json_map.keys().next().cloned().unwrap_or_default(); + map.insert(key.clone(), Value::String(first_key)); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.mapJsonToRegistryNotFound", key = key).to_string())); + } + } + serde_json::to_string(&map).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))? + } else { + input.to_string() + }; + adapter_get(&input, adapted_resource) +} + +fn convert_default_value_to_registry_data(default_value: &Value, reg_type: &RegistryDataType) -> Result { + match (default_value, reg_type) { + (Value::Bool(b), RegistryDataType::Dword) => Ok(RegistryValueData::DWord(if *b { 1 } else { 0 })), + (Value::Number(n), RegistryDataType::Dword) => { + if let Some(u) = n.as_u64() { + Ok(RegistryValueData::DWord(u as u32)) + } else { + Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())) + } + }, + (Value::String(s), RegistryDataType::String) => Ok(RegistryValueData::String(s.clone())), + (Value::String(s), RegistryDataType::ExpandString) => Ok(RegistryValueData::ExpandString(s.clone())), + (Value::Array(a), RegistryDataType::MultiString) => { + let mut result = Vec::new(); + for v in a { + if let Value::String(s) = v { + result.push(s.clone()); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.nonStringInMultiStringDefault", value = v.to_string()).to_string())); + } + } + Ok(RegistryValueData::MultiString(result)) + }, + (Value::Array(a), RegistryDataType::Binary) => { + let mut result = Vec::new(); + for v in a { + if let Value::Number(s) = v { + if let Some(u) = s.as_u64() { + result.push(u as u8); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())); + } + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())); + } + } + Ok(RegistryValueData::Binary(result)) + }, + _ => Err(RegistryResourceError::AdaptedResource(t!("adapter.couldNotConvertDefaultValue", default_value = default_value.to_string(), reg_type = reg_type : {:?}).to_string())), + } +} + +fn convert_registry_value_data_to_mapped_json(value_data: &RegistryValueData, json_type: &JsonType, map: &Map) -> Result { + // use the `map` to reverse convert the `RegistryValueData` back to the original JSON value based on the registry data type + // convert the registry value to the appropriate JSON type based on `json_type` + // for a boolean, a 0 is false and a 1 is true + // a reg_binary for json_type that is an array will be null delimited + let json_value = match (value_data, json_type) { + (RegistryValueData::Binary(byte_vec), JsonType::StringArray) => { + // Pre-build reverse lookup: deserialized bytes -> key name + let reverse_map: Vec<(String, Vec)> = map.iter().filter_map(|(k, v)| { + match serde_json::from_value::>(v.clone()) { + Ok(bytes) => Some((k.clone(), bytes)), + Err(_) => { + warn!("Failed to convert value to Vec: {:?}", v); + None + } + } + }).collect(); + if reverse_map.is_empty() { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.emptyReverseMap").to_string())); + } + let first_value_length = reverse_map.first().map(|(_, b)| b.len()).unwrap_or(0); + let mut result = Vec::new(); + for slice in byte_vec.chunks(first_value_length) { + let matched_key = reverse_map.iter().find_map(|(k, bytes)| { + if bytes == slice { + Some(k.clone()) + } else { + None + } + }); + if let Some(key) = matched_key { + result.push(Value::String(key)); + } else { + let hex_string = slice.iter().map(|b| format!("{:02X}", b)).collect::>().join(" "); + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unmappedByteSlice", hex_string = hex_string).to_string())); + } + } + Value::Array(result) + }, + (RegistryValueData::DWord(dword), JsonType::Boolean) => { + match dword { + 0 => Value::Bool(false), + 1 => Value::Bool(true), + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = dword.to_string(), reg_type = "RegDword").to_string())), + } + }, + (RegistryValueData::DWord(dword), JsonType::String) => { + let mapped_value = map.iter().find_map(|(k, v)| { + if v.as_u64() == Some(*dword as u64) { + Some(k.clone()) + } else { + None + } + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = dword.to_string(), reg_type = "RegDword").to_string()))?; + Value::String(mapped_value) + }, + (RegistryValueData::String(s), JsonType::String) => { + let mapped_key = map.iter().find_map(|(k, v)| { + if v.as_str().is_some_and(|v_str| v_str == s) { + Some(k.clone()) + } else { + None + } + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegString").to_string()))?; + Value::String(mapped_key.to_string()) + }, + (RegistryValueData::String(s), JsonType::Boolean) => { + let mapped_value = map.iter().find_map(|(k, v)| { + if v.as_str().is_some_and(|v_str| v_str == s) { + Some(k.clone()) + } else { + None + } + }).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegString").to_string()))?; + match mapped_value.as_str() { + "true" => Value::Bool(true), + "false" => Value::Bool(false), + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegString").to_string())), + } + }, + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedConversionToJsonType", registry_value_data = format!("{:?}", value_data), json_type = format!("{:?}", json_type)).to_string())), + }; + Ok(json_value) +} + +fn get_registry_value_data(value_name: &str, value: &Value, map: &Map, data_type: &RegistryDataType) -> Result { + let registry_value_data = if let Some(value_array) = value.as_array() { + match data_type { + RegistryDataType::Binary => { + let mut byte_vec = Vec::new(); + for item in value_array.iter() { + if let Some(s) = item.as_str() { + let mapped_value = map.get(s).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegBinary").to_string()))?; + let byte_vec_item = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + byte_vec.extend(byte_vec_item); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", item)).to_string())); + } + } + RegistryValueData::Binary(byte_vec) + }, + RegistryDataType::MultiString => { + let mut string_vec = Vec::new(); + for item in value_array.iter() { + if let Some(s) = item.as_str() { + let mapped_value = map.get(s).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = s, reg_type = "RegMultiString").to_string()))?; + let string_vec_item = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", mapped_value)).to_string()))?.to_string(); + string_vec.push(string_vec_item); + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", item)).to_string())); + } + } + RegistryValueData::MultiString(string_vec) + }, + _ => return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", value)).to_string())), + } + } else { + let value_str = if let Some(s) = value.as_str() { + s.to_string() + } else if value.is_number() || value.is_boolean() { + value.to_string() + } else { + return Err(RegistryResourceError::AdaptedResource(t!("adapter.unsupportedValueType", value_name = value_name, value = format!("{:?}", value)).to_string())); + }; + match data_type { + RegistryDataType::Binary => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegBinary").to_string()))?; + let byte_vec = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + RegistryValueData::Binary(byte_vec) + }, + RegistryDataType::Dword => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegDword").to_string()))?; + let dword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotU64", value = value_str).to_string()))? as u32; + RegistryValueData::DWord(dword) + }, + RegistryDataType::ExpandString => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegExpandString").to_string()))?; + let expand_string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotString", value = value_str).to_string()))?.to_string(); + RegistryValueData::ExpandString(expand_string) + }, + RegistryDataType::MultiString => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegMultiString").to_string()))?; + let multi_string = serde_json::from_value::>(mapped_value.clone()).map_err(|e| RegistryResourceError::AdaptedResource(e.to_string()))?; + RegistryValueData::MultiString(multi_string) + }, + RegistryDataType::Qword => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegQword").to_string()))?; + let qword = mapped_value.as_u64().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotU64", value = value_str).to_string()))?; + RegistryValueData::QWord(qword) + }, + RegistryDataType::String => { + let mapped_value = map.get(&value_str).ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.valueMappingNotFound", value = value_str, reg_type = "RegString").to_string()))?; + let string = mapped_value.as_str().ok_or_else(|| RegistryResourceError::AdaptedResource(t!("adapter.mappedValueNotString", value = value_str).to_string()))?.to_string(); + RegistryValueData::String(string) + }, + } + }; + Ok(registry_value_data) +} diff --git a/resources/registry/src/args.rs b/resources/registry/src/args.rs index 08e36f349..7fef7be80 100644 --- a/resources/registry/src/args.rs +++ b/resources/registry/src/args.rs @@ -5,13 +5,38 @@ use clap::{Parser, Subcommand}; use rust_i18n::t; #[derive(Parser)] -#[clap(name = "registry", version = "0.0.1", about = t!("args.about").to_string(), long_about = None)] +#[clap(name = "registry", version = env!("CARGO_PKG_VERSION"), about = t!("args.about").to_string(), long_about = None)] pub struct Arguments { #[clap(subcommand)] pub subcommand: SubCommand, } +#[derive(Debug, PartialEq, Eq, Subcommand)] +pub enum AdapterSubCommand { + #[clap(name = "get", about = t!("args.adapterGetAbout").to_string())] + Get { + #[clap(short, long, required = true, help = t!("args.adapterArgsInputHelp").to_string())] + input: String, + #[clap(short, long, required = true, help = t!("args.adapterArgsAdaptedResourceHelp").to_string())] + adapted_resource: String, + }, + #[clap(name = "set", about = t!("args.adapterSetAbout").to_string())] + Set { + #[clap(short, long, required = true, help = t!("args.adapterArgsInputHelp").to_string())] + input: String, + #[clap(short, long, required = true, help = t!("args.adapterArgsAdaptedResourceHelp").to_string())] + adapted_resource: String, + }, + #[clap(name = "export", about = t!("args.adapterExportAbout").to_string())] + Export { + #[clap(short, long, required = true, help = t!("args.adapterArgsInputHelp").to_string())] + input: String, + #[clap(short, long, required = true, help = t!("args.adapterArgsAdaptedResourceHelp").to_string())] + adapted_resource: String, + }, +} + #[derive(Debug, PartialEq, Eq, Subcommand)] pub enum ConfigSubCommand { #[clap(name = "get", about = t!("args.configGetAbout").to_string())] @@ -79,6 +104,11 @@ pub enum SubCommand { #[clap(long, help = t!("args.findArgsValuesOnlyHelp").to_string())] values_only: bool, }, + #[clap(name = "adapter", about = t!("args.adapterAbout").to_string(), arg_required_else_help = true)] + Adapter { + #[clap(subcommand)] + subcommand: AdapterSubCommand, + }, #[clap(name = "config", about = t!("args.configAbout").to_string(), arg_required_else_help = true)] Config { #[clap(subcommand)] diff --git a/resources/registry/src/error.rs b/resources/registry/src/error.rs new file mode 100644 index 000000000..b2802453b --- /dev/null +++ b/resources/registry/src/error.rs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum RegistryResourceError { + #[error("Adapted resource deserialization error: {0}")] + AdaptedResource(String), + #[error("Registry error: {0}")] + RegistryError(#[from] dsc_lib_registry::error::RegistryError), +} diff --git a/resources/registry/src/main.rs b/resources/registry/src/main.rs index 885188fc1..2f9ebff17 100644 --- a/resources/registry/src/main.rs +++ b/resources/registry/src/main.rs @@ -6,7 +6,8 @@ use crossterm::event; #[cfg(debug_assertions)] use std::env; -use args::Arguments; +use adapter::{adapter_export, adapter_get, adapter_set}; +use args::{AdapterSubCommand, Arguments, ConfigSubCommand, SubCommand}; use clap::Parser; use dsc_lib_registry::{config::Registry, RegistryHelper}; use rust_i18n::t; @@ -16,7 +17,9 @@ use tracing::{error, trace}; use tracing_subscriber::{filter::LevelFilter, prelude::__tracing_subscriber_SubscriberExt, EnvFilter, Layer}; use types::RegistryList; +mod adapter; mod args; +mod error; mod types; rust_i18n::i18n!("locales", fallback = "en-us"); @@ -34,21 +37,47 @@ fn main() { let args = Arguments::parse(); match args.subcommand { - args::SubCommand::Query { key_path, value_name, recurse } => { + SubCommand::Adapter { subcommand } => { + let result = match subcommand { + AdapterSubCommand::Get { input, adapted_resource } => { + adapter_get(&input, &adapted_resource) + }, + AdapterSubCommand::Set { input, adapted_resource } => { + if let Err(e) = adapter_set(&input, &adapted_resource) { + error!("{e}"); + exit(EXIT_REGISTRY_ERROR); + } + exit(EXIT_SUCCESS); + }, + AdapterSubCommand::Export { input, adapted_resource } => { + adapter_export(&input, &adapted_resource) + }, + }; + match result { + Ok(output) => { + println!("{output}"); + }, + Err(err) => { + error!("{err}"); + exit(EXIT_INVALID_INPUT); + } + } + }, + SubCommand::Query { key_path, value_name, recurse } => { trace!("Get key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); }, - args::SubCommand::Set { key_path, value } => { + SubCommand::Set { key_path, value } => { trace!("Set key_path: {key_path}, value: {value}"); }, - args::SubCommand::Remove { key_path, value_name, recurse } => { + SubCommand::Remove { key_path, value_name, recurse } => { trace!("Remove key_path: {key_path}, value_name: {value_name:?}, recurse: {recurse}"); }, - args::SubCommand::Find { key_path, find, recurse, keys_only, values_only } => { + SubCommand::Find { key_path, find, recurse, keys_only, values_only } => { trace!("Find key_path: {key_path}, find: {find}, recurse: {recurse:?}, keys_only: {keys_only:?}, values_only: {values_only:?}"); }, - args::SubCommand::Config { subcommand } => { + SubCommand::Config { subcommand } => { match subcommand { - args::ConfigSubCommand::Get{input, list} => { + ConfigSubCommand::Get{input, list} => { trace!("Get input: {input}"); let mut output = RegistryList { registry_entries: vec![] }; let reg_list = import_input(&input, list); @@ -80,7 +109,7 @@ fn main() { println!("{json}"); exit(EXIT_SUCCESS); }, - args::ConfigSubCommand::Set{input, list, what_if} => { + ConfigSubCommand::Set{input, list, what_if} => { trace!("Set input: {input}, what_if: {what_if}"); let mut output = RegistryList { registry_entries: vec![] }; let reg_list = import_input(&input, list); @@ -141,7 +170,7 @@ fn main() { } exit(EXIT_SUCCESS); }, - args::ConfigSubCommand::Delete{input, what_if} => { + ConfigSubCommand::Delete{input, what_if} => { trace!("Delete input: {input}, what_if: {what_if}"); let mut reg_helper = match RegistryHelper::new_from_json(&input) { Ok(reg_helper) => reg_helper, diff --git a/resources/windows_personalization/.project.data.json b/resources/windows_personalization/.project.data.json new file mode 100644 index 000000000..b11d0636b --- /dev/null +++ b/resources/windows_personalization/.project.data.json @@ -0,0 +1,10 @@ +{ + "Name": "windows_personalization", + "Kind": "Resource", + "SupportedPlatformOS": "Windows", + "CopyFiles": { + "Windows": [ + "windows_personalization.dsc.adaptedResource.yaml" + ] + } +} diff --git a/resources/windows_personalization/personalization_export.tests.ps1 b/resources/windows_personalization/personalization_export.tests.ps1 new file mode 100644 index 000000000..3d93c4690 --- /dev/null +++ b/resources/windows_personalization/personalization_export.tests.ps1 @@ -0,0 +1,32 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Personalization resource export tests' -Skip:(!$IsWindows) { + It 'Export works with no input' { + $properties = @{ + appsUseLightTheme = @($true, $false) + systemUsesLightTheme = @($true, $false) + autoColorization = @($true, $false) + colorPrevalence = @($true, $false) + transparencyEffects = @($true, $false) + # $null included as it's possible to have no visible places in start menu + startMenuVisiblePlaces = @($null, 'Documents', 'Downloads','Music', 'Pictures', 'Videos', 'Network', 'UserProfile', 'Explorer', 'Settings') + startMenuShowRecentList = @($true, $false) + showRecommendedApps = @($true, $false) + taskbarShowBadges = @($true, $false) + desktopTaskbarShowBadges = @($true, $false) + multimonitorTaskbarGroupingMode = @('AlwaysCombineHideLabels', 'CombineWhenTaskbarIsFull', 'NeverCombine') + multimonitorTaskbar = @($true, $false) + multimonitorDesktopTaskbar = @($true, $false) + multimonitorTaskbarMode = @('Duplicate', 'PrimaryAndMonitorWindowIsOn', 'MonitorWindowIsOn') + multimonitorDesktopTaskbarMode = @('Duplicate', 'PrimaryAndMonitorWindowIsOn', 'MonitorWindowIsOn') + } + $out = dsc resource export -r Microsoft.Windows/Personalization 2> $TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + $resultProperties = $out.resources[0].properties.psobject.properties + ($resultProperties | Measure-Object).Count | Should -Be $properties.Count + foreach ($key in $properties.Keys) { + $resultProperties[$key].Value | Should -BeIn $properties[$key] -Because "Property $key has value $($resultProperties[$key].Value) which is not in expected values $($properties[$key])" + } + } +} diff --git a/resources/windows_personalization/personalization_get.tests.ps1 b/resources/windows_personalization/personalization_get.tests.ps1 new file mode 100644 index 000000000..e211b432e --- /dev/null +++ b/resources/windows_personalization/personalization_get.tests.ps1 @@ -0,0 +1,44 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Windows Personalization get tests' { + It 'Convert dword to boolean' -Skip:(!$IsWindows) { + $json = @{ + "appsUseLightTheme" = $true + } | ConvertTo-Json -Compress + $out = dsc resource get -r Microsoft.Windows/Personalization -i $json 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because "dsc resource get failed with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + $out.actualState.appsUseLightTheme | Should -BeIn @($true, $false) + } + + It 'Convert binary to string array' -Skip:(!$IsWindows) { + $json = @{ + "startMenuVisiblePlaces" = @() + } | ConvertTo-Json -Compress + + $existingVisiblePlaces = (Get-ItemProperty -path HKCU:\Software\Microsoft\Windows\CurrentVersion\Start -Name VisiblePlaces).VisiblePlaces + + try { + $allVisiblePlaces = @(134, 8, 115, 82, 170, 81, 67, 66, 159, 123, 39, 118, 88, 70, 89, 212, 206, 213, 52, 45, 90, 250, 67, 69, 130, 242, + 34, 230, 234, 247, 119, 60, 47, 179, 103, 227, 222, 137, 85, 67, 191, 206, 97, 243, 123, 24, 169, 55, 32, 6, 11, 176, 81, 127, 50, 76, + 170, 30, 52, 204, 84, 127, 115, 21, 160, 7, 63, 56, 10, 232, 128, 76, 176, 90, 134, 219, 132, 93, 188, 77, 197, 165, 179, 66, 134, 125, + 244, 66, 128, 164, 147, 250, 202, 122, 136, 181, 68, 129, 117, 254, 13, 8, 174, 66, 139, 218, 52, 237, 151, 182, 99, 148, 74, 176, 189, + 116, 74, 249, 104, 79, 139, 214, 67, 152, 7, 29, 168, 188, 188, 36, 138, 20, 12, 214, 137, 66, 160, 128, 110, 217, 187, 162, 72, 130) + Set-ItemProperty -path HKCU:\Software\Microsoft\Windows\CurrentVersion\Start -Name VisiblePlaces -Value $allVisiblePlaces + $out = dsc resource get -r Microsoft.Windows/Personalization -i $json 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because "dsc resource get failed with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + $out.actualState.startMenuVisiblePlaces | Should -BeExactly @("Settings","Documents","Downloads","Music","Pictures","Videos","Network","UserProfile","Explorer") + } finally { + Set-ItemProperty -path HKCU:\Software\Microsoft\Windows\CurrentVersion\Start -Name VisiblePlaces -Value $existingVisiblePlaces + } + } + + It 'Convert dword to string enum' -Skip:(!$IsWindows) { + $json = @{ + "multimonitorTaskbarGroupingMode" = 'NeverCombine' + } | ConvertTo-Json -Compress + $out = dsc resource get -r Microsoft.Windows/Personalization -i $json 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because "dsc resource get failed with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + $out.actualState.multimonitorTaskbarGroupingMode | Should -BeIn @('NeverCombine', 'AlwaysCombineHideLabels', 'CombineWhenTaskbarIsFull') + } +} diff --git a/resources/windows_personalization/personalization_set.tests.ps1 b/resources/windows_personalization/personalization_set.tests.ps1 new file mode 100644 index 000000000..73a1245a0 --- /dev/null +++ b/resources/windows_personalization/personalization_set.tests.ps1 @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Personalization resource set tests' -Skip:(!$IsWindows) { + BeforeAll { + $currentSettings = dsc resource export -r Microsoft.Windows/Personalization 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because "Failed to export current personalization settings with exit code $LASTEXITCODE. Error log: $(Get-Content $TestDrive/error.log -Raw)" + } + + AfterAll { + dsc config set -i $currentSettings 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + } + + It 'Can set personalization settings' { + $newSettings = @{ + appsUseLightTheme = $true + systemUsesLightTheme = $true + autoColorization = $true + colorPrevalence = $true + transparencyEffects = $true + startMenuVisiblePlaces = @('Documents', 'Downloads','Music', 'Pictures', 'Videos', 'Network', 'UserProfile', 'Explorer', 'Settings') + startMenuShowRecentList = $true + showRecommendedApps = $true + taskbarShowBadges = $true + desktopTaskbarShowBadges = $true + multimonitorTaskbarGroupingMode = 'CombineWhenTaskbarIsFull' + multimonitorTaskbar = $true + multimonitorDesktopTaskbar = $true + multimonitorTaskbarMode = 'PrimaryAndMonitorWindowIsOn' + multimonitorDesktopTaskbarMode = 'MonitorWindowIsOn' + } + + dsc resource set -r Microsoft.Windows/Personalization -i ($newSettings | ConvertTo-Json) 2>$TestDrive/error.log + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + + $out = dsc resource export -r Microsoft.Windows/Personalization 2>$TestDrive/error.log | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw) + foreach ($key in $newSettings.Keys) { + $keyValue = $out.resources[0].properties.$key + $expectedValue = $newSettings.$key + $keyValue | Should -Be $expectedValue -Because "Property $key has value $keyValue which is not equal to expected value $expectedValue" + } + } +} diff --git a/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml new file mode 100644 index 000000000..1628ab6ba --- /dev/null +++ b/resources/windows_personalization/windows_personalization.dsc.adaptedResource.yaml @@ -0,0 +1,262 @@ +# Adapted Registry resource for personalization settings defined in: +# https://learn.microsoft.com/en-us/windows/apps/develop/settings/settings-common#personalization---colors + +$schema: https://aka.ms/dsc/schemas/v3/bundled/adaptedresource/manifest.json +type: Microsoft.Windows/Personalization +kind: resource +version: 1.0.0 +capabilities: +- get +- set +description: Controls Windows personalization settings such as accent color, light/dark mode, and transparency effects. +author: Microsoft Corporation +requireAdapter: Microsoft.Windows.Adapter/Registry +content: + appsUseLightTheme: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: AppsUseLightTheme + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + systemUsesLightTheme: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: SystemUsesLightTheme + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + autoColorization: + keyPath: HKCU\Control Panel\Desktop + valueType: REG_DWORD + valueName: AutoColorization + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + colorPrevalence: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: ColorPrevalence + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + transparencyEffects: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize + valueType: REG_DWORD + valueName: EnableTransparency + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + startMenuVisiblePlaces: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Start + valueType: REG_BINARY + valueName: VisiblePlaces + jsonType: stringArray + defaultValueIfNotFound: [] + mapJsonToRegistry: + # Documents: {2D34D5CE-FA5A-4543-82F2-22E6EAF7773C} + Documents: [0xCE, 0xD5, 0x34, 0x2D, 0x5A, 0xFA, 0x43, 0x45, 0x82, 0xF2, 0x22, 0xE6, 0xEA, 0xF7, 0x77, 0x3C] + # Downloads: {E367B32F-89DE-4355-BFCE-61F37B18A937} + Downloads: [0x2F, 0xB3, 0x67, 0xE3, 0xDE, 0x89, 0x55, 0x43, 0xBF, 0xCE, 0x61, 0xF3, 0x7B, 0x18, 0xA9, 0x37] + # Music: {B00B0620-7F51-4C32-AA1E-34CC547F7315} + Music: [0x20, 0x06, 0x0B, 0xB0, 0x51, 0x7F, 0x32, 0x4C, 0xAA, 0x1E, 0x34, 0xCC, 0x54, 0x7F, 0x73, 0x15] + # Pictures: {383F07A0-E80A-4C80-B05A-86DB845DBC4D} + Pictures: [0xA0, 0x07, 0x3F, 0x38, 0x0A, 0xE8, 0x80, 0x4C, 0xB0, 0x5A, 0x86, 0xDB, 0x84, 0x5D, 0xBC, 0x4D] + # Videos: {42B3A5C5-7D86-42F4-80A4-93FACA7A88B5} + Videos: [0xC5, 0xA5, 0xB3, 0x42, 0x86, 0x7D, 0xF4, 0x42, 0x80, 0xA4, 0x93, 0xFA, 0xCA, 0x7A, 0x88, 0xB5] + # Network: {FE758144-080D-42AE-8BDA-34ED97B66394} + Network: [0x44, 0x81, 0x75, 0xFE, 0x0D, 0x08, 0xAE, 0x42, 0x8B, 0xDA, 0x34, 0xED, 0x97, 0xB6, 0x63, 0x94] + # UserProfile: {74BDB04A-F94A-4F68-8BD6-4398071DA8BC} + UserProfile: [0x4A, 0xB0, 0xBD, 0x74, 0x4A, 0xF9, 0x68, 0x4F, 0x8B, 0xD6, 0x43, 0x98, 0x07, 0x1D, 0xA8, 0xBC] + # Explorer: {148A24BC-D60C-4289-A080-6ED9BBA24882} + Explorer: [0xBC, 0x24, 0x8A, 0x14, 0x0C, 0xD6, 0x89, 0x42, 0xA0, 0x80, 0x6E, 0xD9, 0xBB, 0xA2, 0x48, 0x82] + # Settings: {52730886-51AA-4243-9F7B-2776584659D4} + Settings: [0x86, 0x08, 0x73, 0x52, 0xAA, 0x51, 0x43, 0x42, 0x9F, 0x7B, 0x27, 0x76, 0x58, 0x46, 0x59, 0xD4] + startMenuShowRecentList: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Start + valueType: REG_DWORD + valueName: ShowRecentList + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + showRecommendedApps: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced + valueType: REG_DWORD + valueName: Start_TrackDocs + jsonType: boolean + defaultValueIfNotFound: 0 + mapJsonToRegistry: + 'false': 0 + 'true': 1 + taskbarShowBadges: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + valueType: REG_SZ + valueName: SystemSettings_Taskbar_Badging + jsonType: boolean + defaultValueIfNotFound: '0' + mapJsonToRegistry: + 'false': '0' + 'true': '1' + desktopTaskbarShowBadges: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\TaskbarBadges + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_Badging + jsonType: boolean + defaultValueIfNotFound: '0' + mapJsonToRegistry: + 'false': '0' + 'true': '1' + multimonitorTaskbarGroupingMode: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarGlomLevel + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_GroupingMode + jsonType: string + defaultValueIfNotFound: '0' + mapJsonToRegistry: + AlwaysCombineHideLabels: '0' + CombineWhenTaskbarIsFull: '1' + NeverCombine: '2' + multimonitorTaskbar: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarEnabled + valueType: REG_SZ + valueName: SystemSettings_Taskbar_MultiMon + jsonType: boolean + defaultValueIfNotFound: '0' + mapJsonToRegistry: + 'false': '0' + 'true': '1' + multimonitorDesktopTaskbar: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarEnabled + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_MultiMon + jsonType: boolean + defaultValueIfNotFound: '0' + mapJsonToRegistry: + 'false': '0' + 'true': '1' + multimonitorTaskbarMode: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarMode + valueType: REG_SZ + valueName: SystemSettings_Taskbar_MultiMonTaskbarMode + jsonType: string + defaultValueIfNotFound: '0' + mapJsonToRegistry: + Duplicate: '0' + PrimaryAndMonitorWindowIsOn: '1' + MonitorWindowIsOn: '2' + multimonitorDesktopTaskbarMode: + keyPath: HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced\MMTaskbarMode + valueType: REG_SZ + valueName: SystemSettings_DesktopTaskbar_MultiMonTaskbarMode + jsonType: string + defaultValueIfNotFound: '0' + mapJsonToRegistry: + Duplicate: '0' + PrimaryAndMonitorWindowIsOn: '1' + MonitorWindowIsOn: '2' +schema: + embedded: + $schema: http://json-schema.org/draft-07/schema# + $id: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/resources/Adapted/Three/v0.1.0/schema.json + title: Microsoft.Windows/Personalization + description: Windows personalization settings such as accent color, light/dark mode, and transparency effects as described in https://learn.microsoft.com/windows/apps/develop/settings/settings-common + type: object + required: [] + additionalProperties: false + properties: + appsUseLightTheme: + type: boolean + title: App Uses Light Theme + description: Indicates whether the app uses the light theme. + systemUsesLightTheme: + type: boolean + title: System Uses Light Theme + description: Indicates whether the system uses the light theme. + autoColorization: + type: boolean + title: Auto Colorization + description: Signifies auto-apply accent color based on background or manually. + colorPrevalence: + type: boolean + title: Color Prevalence + description: Indicates whether the accent color is applied to Start, taskbar, and action center + transparencyEffects: + type: boolean + title: Transparency Effects + description: Indicates whether transparency effects are enabled. + startMenuVisiblePlaces: + type: array + title: Start Menu Visible Places + description: A list of places that are visible in the Start menu. + items: + type: string + enum: + - Documents + - Downloads + - Music + - Pictures + - Videos + - Network + - UserProfile + - Explorer + - Settings + startMenuShowRecentList: + type: boolean + title: Start Menu Show Recent List + description: Specifies whether apps that were recently installed in Start in various surfaces are shown on Start. + showRecommendedApps: + type: boolean + title: Show Recommended Apps + description: Specifies whether recommended files in Start, recent files in File Explorer, and items in Jump Lists are shown. + taskbarShowBadges: + type: boolean + title: Taskbar Show Badges + description: Specifies whether badges on taskbar buttons are shown. + desktopTaskbarShowBadges: + type: boolean + title: Desktop Taskbar Show Badges + description: Specifies whether badges on taskbar buttons are shown on the desktop. + multimonitorTaskbarGroupingMode: + type: string + title: Multimonitor Taskbar Grouping Mode + description: Specifies how taskbar buttons are grouped on multiple displays. + enum: + - AlwaysCombineHideLabels + - CombineWhenTaskbarIsFull + - NeverCombine + multimonitorTaskbar: + type: boolean + title: Multimonitor Taskbar + description: Enables showing the taskbar on multiple displays. + multimonitorDesktopTaskbar: + type: boolean + title: Multimonitor Desktop Taskbar + description: Enables showing the desktop taskbar on multiple displays. + multimonitorTaskbarMode: + type: string + title: Multimonitor Taskbar Mode + description: Specifies the mode of the taskbar on multiple displays. + enum: + - Duplicate + - PrimaryAndMonitorWindowIsOn + - MonitorWindowIsOn + multimonitorDesktopTaskbarMode: + type: string + title: Multimonitor Desktop Taskbar Mode + description: Specifies the mode of the desktop taskbar on multiple displays. + enum: + - Duplicate + - PrimaryAndMonitorWindowIsOn + - MonitorWindowIsOn diff --git a/tools/test_group_resource/src/main.rs b/tools/test_group_resource/src/main.rs index 31b8520c9..60176a3c9 100644 --- a/tools/test_group_resource/src/main.rs +++ b/tools/test_group_resource/src/main.rs @@ -27,6 +27,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), require_adapter: Some("Test/TestGroup".parse().unwrap()), + adapted_content: None, target_resource: None, schema: None, manifest: Some(ResourceManifest { @@ -55,6 +56,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), require_adapter: Some("Test/TestGroup".parse().unwrap()), + adapted_content: None, target_resource: None, schema: None, manifest: Some(ResourceManifest { @@ -87,6 +89,7 @@ fn main() { author: Some("Microsoft".to_string()), properties: Some(vec!["Property1".to_string(), "Property2".to_string()]), require_adapter: None, + adapted_content: None, target_resource: None, manifest: None, schema: None,