diff --git a/dsc/tests/dsc_export.tests.ps1 b/dsc/tests/dsc_export.tests.ps1 index 01e55b2ba..5f930cd8e 100644 --- a/dsc/tests/dsc_export.tests.ps1 +++ b/dsc/tests/dsc_export.tests.ps1 @@ -181,4 +181,42 @@ resources: $out.resources[0].properties.psobject.properties.name | Should -Not -Contain '_securityContext' $out.resources[0].properties.psobject.properties.name | Should -Not -Contain '_name' } + + It 'Export can be used with a resource that only implements Get with filter' { + $yaml = @' +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: NoExport + type: Test/Get + properties: + name: two +'@ + $out = dsc config export -i $yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.resources.count | Should -Be 1 + $out.resources[0].type | Should -BeExactly 'Test/Get' + $out.resources[0].properties.name | Should -BeExactly 'two' + $out.resources[0].properties.id | Should -Be 2 + } + + It 'Export can be used with a resource that only implements Get with no filter' { + $yaml = @' +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: OS + type: Microsoft/OSInfo +'@ + $out = dsc config export -i $yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.resources.count | Should -Be 1 + $out.resources[0].type | Should -BeExactly 'Microsoft/OSInfo' + $expectedOs = if ($IsWindows) { + 'Windows' + } elseif ($IsMacOS) { + 'macOS' + } else { + 'Linux' + } + $out.resources[0].properties.family | Should -BeExactly $expectedOs + } } diff --git a/dsc/tests/dsc_resource_list.tests.ps1 b/dsc/tests/dsc_resource_list.tests.ps1 index 8ee75845a..8b6e14175 100644 --- a/dsc/tests/dsc_resource_list.tests.ps1 +++ b/dsc/tests/dsc_resource_list.tests.ps1 @@ -84,10 +84,12 @@ Describe 'Tests for listing resources' { } It 'Capabilities are returned' { - $resource = dsc resource list Microsoft/OSInfo | ConvertFrom-Json + $resource = dsc resource list Microsoft.DSC.Debug/Echo | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $resource.capabilities.Count | Should -Be 2 + $resource.capabilities.Count | Should -Be 4 $resource.capabilities | Should -Contain 'get' + $resource.capabilities | Should -Contain 'set' + $resource.capabilities | Should -Contain 'test' $resource.capabilities | Should -Contain 'export' } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index f18aa3075..80c5be585 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -146,6 +146,7 @@ resourceInvalidJson = "Resource reported input JSON is not valid" invalidArrayKey = "Unsupported array value for key '%{key}'. Only string and number is supported." invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, and array is supported." inDesiredStateNotBool = "'_inDesiredState' is not a boolean" +exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation" [dscresources.dscresource] invokeGet = "Invoking get for '%{resource}'" diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index cf4d57156..51f1c8522 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -487,6 +487,26 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result) -> Result { let Some(export) = resource.export.as_ref() else { + // see if get is supported and use that instead + if resource.get.is_some() { + info!("{}", t!("dscresources.commandResource.exportNotSupportedUsingGet", resource = &resource.resource_type)); + let get_result = invoke_get(resource, cwd, input.unwrap_or(""))?; + let mut instances: Vec = Vec::new(); + match get_result { + GetResult::Group(group_response) => { + for result in group_response { + instances.push(serde_json::to_value(result)?); + } + }, + GetResult::Resource(response) => { + instances.push(response.actual_state); + } + } + return Ok(ExportResult { + actual_state: instances, + }); + } + // if neither export nor get is supported, return an error return Err(DscError::Operation(t!("dscresources.commandResource.exportNotSupported", resource = &resource.resource_type).to_string())) }; @@ -840,7 +860,7 @@ fn json_to_hashmap(json: &str) -> Result, DscError> { }, Value::Null => { // ignore null values - }, + }, Value::Object(_) => { return Err(DscError::Operation(t!("dscresources.commandResource.invalidKey", key = key).to_string())); }, diff --git a/osinfo/osinfo.dsc.resource.json b/osinfo/osinfo.dsc.resource.json index 037355fa4..9989270b6 100644 --- a/osinfo/osinfo.dsc.resource.json +++ b/osinfo/osinfo.dsc.resource.json @@ -12,9 +12,6 @@ "get": { "executable": "osinfo" }, - "export": { - "executable": "osinfo" - }, "schema": { "embedded": { "$schema": "http://json-schema.org/draft-07/schema#", diff --git a/tools/dsctest/dscget.dsc.resource.json b/tools/dsctest/dscget.dsc.resource.json new file mode 100644 index 000000000..eaed3821e --- /dev/null +++ b/tools/dsctest/dscget.dsc.resource.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Test/Get", + "version": "0.1.0", + "get": { + "executable": "dsctest", + "args": [ + "get", + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "schema": { + "command": { + "executable": "dsctest", + "args": [ + "schema", + "-s", + "get" + ] + } + } +} diff --git a/tools/dsctest/src/args.rs b/tools/dsctest/src/args.rs index 028d2cdc5..33dad4325 100644 --- a/tools/dsctest/src/args.rs +++ b/tools/dsctest/src/args.rs @@ -10,6 +10,7 @@ pub enum Schemas { ExitCode, Export, Exporter, + Get, InDesiredState, Metadata, Sleep, @@ -57,6 +58,12 @@ pub enum SubCommand { input: String, }, + #[clap(name = "get", about = "Get a resource")] + Get { + #[clap(name = "input", short, long, help = "The input to the get command as JSON")] + input: String, + }, + #[clap(name = "in-desired-state", about = "Specify if the resource is in the desired state")] InDesiredState { #[clap(name = "input", short, long, help = "The input to the in desired state command as JSON")] diff --git a/tools/dsctest/src/get.rs b/tools/dsctest/src/get.rs new file mode 100644 index 000000000..39c425c4b --- /dev/null +++ b/tools/dsctest/src/get.rs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] +#[serde(deny_unknown_fields)] +pub struct Get { + pub name: Option, + pub id: Option, +} diff --git a/tools/dsctest/src/main.rs b/tools/dsctest/src/main.rs index 2d1777a8f..9486e117d 100644 --- a/tools/dsctest/src/main.rs +++ b/tools/dsctest/src/main.rs @@ -7,6 +7,7 @@ mod exist; mod exit_code; mod export; mod exporter; +mod get; mod in_desired_state; mod metadata; mod sleep; @@ -22,6 +23,7 @@ use crate::exist::{Exist, State}; use crate::exit_code::ExitCode; use crate::export::Export; use crate::exporter::{Exporter, Resource}; +use crate::get::Get; use crate::in_desired_state::InDesiredState; use crate::metadata::Metadata; use crate::sleep::Sleep; @@ -113,6 +115,45 @@ fn main() { } String::new() }, + SubCommand::Get { input } => { + let get = match serde_json::from_str::(&input) { + Ok(get) => get, + Err(err) => { + eprintln!("Error JSON does not match schema: {err}"); + std::process::exit(1); + } + }; + let instances = vec![ + Get { + name : Some("one".to_string()), + id: Some(1), + }, + Get { + name : Some("two".to_string()), + id: Some(2), + }, + Get { + name : Some("three".to_string()), + id: Some(3), + }, + ]; + // depending on the input, return the appropriate instance whether it is name or id or both + let resource = if let Some(name) = get.name { + instances.into_iter().find(|i| i.name.as_ref() == Some(&name)).unwrap_or_else(|| { + eprintln!("No instance found with name: {name}"); + std::process::exit(1); + }) + } else if let Some(id) = get.id { + instances.into_iter().find(|i| i.id == Some(id)).unwrap_or_else(|| { + eprintln!("No instance found with id: {id}"); + std::process::exit(1); + }) + } else { + eprintln!("No name or id provided in input"); + std::process::exit(1); + }; + serde_json::to_string(&resource).unwrap() + }, SubCommand::InDesiredState { input } => { let mut in_desired_state = match serde_json::from_str::(&input) { Ok(in_desired_state) => in_desired_state, @@ -162,6 +203,9 @@ fn main() { Schemas::Exporter => { schema_for!(Exporter) }, + Schemas::Get => { + schema_for!(Get) + }, Schemas::InDesiredState => { schema_for!(InDesiredState) },