Skip to content

Commit 7d466d8

Browse files
authored
Merge pull request #1035 from SteveL-MSFT/get-as-export
Add support for using `get` if resource doesn't implement `export`
2 parents 0ea091c + 54f8c7a commit 7d466d8

File tree

8 files changed

+157
-3
lines changed

8 files changed

+157
-3
lines changed

dsc/tests/dsc_export.tests.ps1

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,4 +181,36 @@ resources:
181181
$out.resources[0].properties.psobject.properties.name | Should -Not -Contain '_securityContext'
182182
$out.resources[0].properties.psobject.properties.name | Should -Not -Contain '_name'
183183
}
184+
185+
It 'Export can be used with a resource that only implements Get with filter' {
186+
$yaml = @'
187+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
188+
resources:
189+
- name: NoExport
190+
type: Test/Get
191+
properties:
192+
name: two
193+
'@
194+
$out = dsc config export -i $yaml | ConvertFrom-Json
195+
$LASTEXITCODE | Should -Be 0
196+
$out.resources.count | Should -Be 1
197+
$out.resources[0].type | Should -BeExactly 'Test/Get'
198+
$out.resources[0].properties.name | Should -BeExactly 'two'
199+
$out.resources[0].properties.id | Should -Be 2
200+
}
201+
202+
It 'Export can be used with a resource that only implements Get with no filter' {
203+
$yaml = @'
204+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
205+
resources:
206+
- name: NoFilter
207+
type: Test/Get
208+
'@
209+
$out = dsc config export -i $yaml | ConvertFrom-Json
210+
$LASTEXITCODE | Should -Be 0
211+
$out.resources.count | Should -Be 1
212+
$out.resources[0].type | Should -BeExactly 'Test/Get'
213+
$out.resources[0].properties.name | Should -BeExactly 'one'
214+
$out.resources[0].properties.id | Should -Be 1
215+
}
184216
}

dsc/tests/dsc_resource_list.tests.ps1

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,12 @@ Describe 'Tests for listing resources' {
8484
}
8585

8686
It 'Capabilities are returned' {
87-
$resource = dsc resource list Microsoft/OSInfo | ConvertFrom-Json
87+
$resource = dsc resource list Microsoft.DSC.Debug/Echo | ConvertFrom-Json
8888
$LASTEXITCODE | Should -Be 0
89-
$resource.capabilities.Count | Should -Be 2
89+
$resource.capabilities.Count | Should -Be 4
9090
$resource.capabilities | Should -Contain 'get'
91+
$resource.capabilities | Should -Contain 'set'
92+
$resource.capabilities | Should -Contain 'test'
9193
$resource.capabilities | Should -Contain 'export'
9294
}
9395

dsc_lib/locales/en-us.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ resourceInvalidJson = "Resource reported input JSON is not valid"
146146
invalidArrayKey = "Unsupported array value for key '%{key}'. Only string and number is supported."
147147
invalidKey = "Unsupported value for key '%{key}'. Only string, bool, number, and array is supported."
148148
inDesiredStateNotBool = "'_inDesiredState' is not a boolean"
149+
exportNotSupportedUsingGet = "Export is not supported by resource '%{resource}' using get operation"
149150

150151
[dscresources.dscresource]
151152
invokeGet = "Invoking get for '%{resource}'"

dsc_lib/src/dscresources/command_resource.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,26 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result<String, DscE
487487
/// Error returned if the resource does not successfully export the current state
488488
pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str>) -> Result<ExportResult, DscError> {
489489
let Some(export) = resource.export.as_ref() else {
490+
// see if get is supported and use that instead
491+
if resource.get.is_some() {
492+
info!("{}", t!("dscresources.commandResource.exportNotSupportedUsingGet", resource = &resource.resource_type));
493+
let get_result = invoke_get(resource, cwd, input.unwrap_or(""))?;
494+
let mut instances: Vec<Value> = Vec::new();
495+
match get_result {
496+
GetResult::Group(group_response) => {
497+
for result in group_response {
498+
instances.push(serde_json::to_value(result)?);
499+
}
500+
},
501+
GetResult::Resource(response) => {
502+
instances.push(response.actual_state);
503+
}
504+
}
505+
return Ok(ExportResult {
506+
actual_state: instances,
507+
});
508+
}
509+
// if neither export nor get is supported, return an error
490510
return Err(DscError::Operation(t!("dscresources.commandResource.exportNotSupported", resource = &resource.resource_type).to_string()))
491511
};
492512

@@ -840,7 +860,7 @@ fn json_to_hashmap(json: &str) -> Result<HashMap<String, String>, DscError> {
840860
},
841861
Value::Null => {
842862
// ignore null values
843-
},
863+
},
844864
Value::Object(_) => {
845865
return Err(DscError::Operation(t!("dscresources.commandResource.invalidKey", key = key).to_string()));
846866
},
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
3+
"type": "Test/Get",
4+
"version": "0.1.0",
5+
"get": {
6+
"executable": "dsctest",
7+
"args": [
8+
"get",
9+
{
10+
"jsonInputArg": "--input",
11+
"mandatory": true
12+
}
13+
]
14+
},
15+
"schema": {
16+
"command": {
17+
"executable": "dsctest",
18+
"args": [
19+
"schema",
20+
"-s",
21+
"get"
22+
]
23+
}
24+
}
25+
}

tools/dsctest/src/args.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub enum Schemas {
1010
ExitCode,
1111
Export,
1212
Exporter,
13+
Get,
1314
InDesiredState,
1415
Metadata,
1516
Sleep,
@@ -57,6 +58,12 @@ pub enum SubCommand {
5758
input: String,
5859
},
5960

61+
#[clap(name = "get", about = "Get a resource")]
62+
Get {
63+
#[clap(name = "input", short, long, help = "The input to the get command as JSON")]
64+
input: String,
65+
},
66+
6067
#[clap(name = "in-desired-state", about = "Specify if the resource is in the desired state")]
6168
InDesiredState {
6269
#[clap(name = "input", short, long, help = "The input to the in desired state command as JSON")]

tools/dsctest/src/get.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use schemars::JsonSchema;
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
8+
#[serde(deny_unknown_fields)]
9+
pub struct Get {
10+
pub name: Option<String>,
11+
pub id: Option<i32>,
12+
}

tools/dsctest/src/main.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod exist;
77
mod exit_code;
88
mod export;
99
mod exporter;
10+
mod get;
1011
mod in_desired_state;
1112
mod metadata;
1213
mod sleep;
@@ -22,6 +23,7 @@ use crate::exist::{Exist, State};
2223
use crate::exit_code::ExitCode;
2324
use crate::export::Export;
2425
use crate::exporter::{Exporter, Resource};
26+
use crate::get::Get;
2527
use crate::in_desired_state::InDesiredState;
2628
use crate::metadata::Metadata;
2729
use crate::sleep::Sleep;
@@ -113,6 +115,56 @@ fn main() {
113115
}
114116
String::new()
115117
},
118+
SubCommand::Get { input } => {
119+
let instances = vec![
120+
Get {
121+
name : Some("one".to_string()),
122+
id: Some(1),
123+
},
124+
Get {
125+
name : Some("two".to_string()),
126+
id: Some(2),
127+
},
128+
Get {
129+
name : Some("three".to_string()),
130+
id: Some(3),
131+
},
132+
];
133+
134+
let resource = if input.is_empty() {
135+
// If neither name nor id is provided, return the first instance
136+
instances.into_iter().next().unwrap_or_else(|| {
137+
eprintln!("No instances found");
138+
std::process::exit(1);
139+
})
140+
} else {
141+
let get = match serde_json::from_str::<Get>(&input) {
142+
Ok(get) => get,
143+
Err(err) => {
144+
eprintln!("Error JSON does not match schema: {err}");
145+
std::process::exit(1);
146+
}
147+
};
148+
// depending on the input, return the appropriate instance whether it is name or id or both
149+
if let Some(name) = get.name {
150+
instances.into_iter().find(|i| i.name.as_ref() == Some(&name)).unwrap_or_else(|| {
151+
eprintln!("No instance found with name: {name}");
152+
std::process::exit(1);
153+
})
154+
} else if let Some(id) = get.id {
155+
instances.into_iter().find(|i| i.id == Some(id)).unwrap_or_else(|| {
156+
eprintln!("No instance found with id: {id}");
157+
std::process::exit(1);
158+
})
159+
} else {
160+
instances.into_iter().next().unwrap_or_else(|| {
161+
eprintln!("No instances found");
162+
std::process::exit(1);
163+
})
164+
}
165+
};
166+
serde_json::to_string(&resource).unwrap()
167+
},
116168
SubCommand::InDesiredState { input } => {
117169
let mut in_desired_state = match serde_json::from_str::<in_desired_state::InDesiredState>(&input) {
118170
Ok(in_desired_state) => in_desired_state,
@@ -162,6 +214,9 @@ fn main() {
162214
Schemas::Exporter => {
163215
schema_for!(Exporter)
164216
},
217+
Schemas::Get => {
218+
schema_for!(Get)
219+
},
165220
Schemas::InDesiredState => {
166221
schema_for!(InDesiredState)
167222
},

0 commit comments

Comments
 (0)