Skip to content

Commit 9461eff

Browse files
authored
Merge branch 'main' into manifest-version
2 parents 1cbb569 + 57f2e89 commit 9461eff

File tree

6 files changed

+249
-19
lines changed

6 files changed

+249
-19
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'tests for resource input' {
5+
BeforeAll {
6+
$manifest = @'
7+
{
8+
"manifestVersion": "1.0.0",
9+
"type": "Test/EnvVarInput",
10+
"version": "0.1.0",
11+
"get": {
12+
"executable": "pwsh",
13+
"input": "env",
14+
"args": [
15+
"-NoLogo",
16+
"-NonInteractive",
17+
"-NoProfile",
18+
"-Command",
19+
"\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\""
20+
]
21+
},
22+
"set": {
23+
"executable": "pwsh",
24+
"input": "env",
25+
"args": [
26+
"-NoLogo",
27+
"-NonInteractive",
28+
"-NoProfile",
29+
"-Command",
30+
"\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\""
31+
],
32+
"return": "state",
33+
"implementsPretest": true
34+
},
35+
"test": {
36+
"executable": "pwsh",
37+
"input": "env",
38+
"args": [
39+
"-NoLogo",
40+
"-NonInteractive",
41+
"-NoProfile",
42+
"-Command",
43+
"\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\""
44+
]
45+
},
46+
"schema": {
47+
"embedded": {
48+
"$schema": "http://json-schema.org/draft-07/schema#",
49+
"$id": "https://test",
50+
"title": "test",
51+
"description": "test",
52+
"type": "object",
53+
"required": [],
54+
"additionalProperties": false,
55+
"properties": {
56+
"Hello": {
57+
"type": "string",
58+
"description": "test"
59+
},
60+
"World": {
61+
"type": "number",
62+
"description": "test"
63+
},
64+
"Boolean": {
65+
"type": "boolean",
66+
"description": "test"
67+
},
68+
"StringArray": {
69+
"type": "array",
70+
"description": "test",
71+
"items": {
72+
"type": "string"
73+
}
74+
},
75+
"NumberArray": {
76+
"type": "array",
77+
"description": "test",
78+
"items": {
79+
"type": "number"
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
'@
87+
$oldPath = $env:DSC_RESOURCE_PATH
88+
$env:DSC_RESOURCE_PATH = $TestDrive
89+
Set-Content $TestDrive/EnvVarInput.dsc.resource.json -Value $manifest
90+
}
91+
92+
AfterAll {
93+
$env:DSC_RESOURCE_PATH = $oldPath
94+
}
95+
96+
It 'Input can be sent to the resource for: <operation>' -TestCases @(
97+
@{ operation = 'get'; member = 'actualState' }
98+
@{ operation = 'set'; member = 'afterState' }
99+
@{ operation = 'test'; member = 'actualState' }
100+
) {
101+
param($operation, $member)
102+
103+
$json = @"
104+
{
105+
"Hello": "foo",
106+
"World": 2,
107+
"Boolean": true,
108+
"StringArray": ["foo", "bar"],
109+
"NumberArray": [1, 2, 3]
110+
}
111+
"@
112+
113+
$result = $json | dsc resource $operation -r Test/EnvVarInput | ConvertFrom-Json
114+
$result.$member.Hello | Should -BeExactly 'foo'
115+
$result.$member.World | Should -Be 2
116+
$result.$member.Boolean | Should -Be 'true'
117+
$result.$member.StringArray | Should -BeExactly 'foo,bar'
118+
$result.$member.NumberArray | Should -BeExactly '1,2,3'
119+
}
120+
}

dsc_lib/src/discovery/command_discovery.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ impl ResourceDiscovery for CommandDiscovery {
9393
let manifest = import_manifest(provider_resource.manifest.clone().unwrap())?;
9494
// invoke the list command
9595
let list_command = manifest.provider.unwrap().list;
96-
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&provider_resource.directory))
96+
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&provider_resource.directory), None)
9797
{
9898
Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr),
9999
Err(e) => {

dsc_lib/src/dscresources/command_resource.rs

Lines changed: 116 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33

44
use jsonschema::JSONSchema;
55
use serde_json::Value;
6-
use std::{process::Command, io::{Write, Read}, process::Stdio};
7-
6+
use std::{collections::HashMap, process::Command, io::{Write, Read}, process::Stdio};
87
use crate::dscerror::DscError;
9-
use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}};
8+
use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, InputKind, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}};
109

1110
pub const EXIT_PROCESS_TERMINATED: i32 = 0x102;
1211

@@ -21,12 +20,27 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 0x102;
2120
///
2221
/// Error returned if the resource does not successfully get the current state
2322
pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Result<GetResult, DscError> {
24-
if !filter.is_empty() && resource.get.input.is_some() {
23+
let input_kind = if let Some(input_kind) = &resource.get.input {
24+
input_kind.clone()
25+
}
26+
else {
27+
InputKind::Stdin
28+
};
29+
30+
let mut env: Option<HashMap<String, String>> = None;
31+
let mut input_filter: Option<&str> = None;
32+
if !filter.is_empty() {
2533
verify_json(resource, cwd, filter)?;
34+
35+
if input_kind == InputKind::Env {
36+
env = Some(json_to_hashmap(filter)?);
37+
}
38+
else {
39+
input_filter = Some(filter);
40+
}
2641
}
2742

28-
let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), Some(filter), Some(cwd))?;
29-
//println!("{stdout}");
43+
let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), input_filter, Some(cwd), env)?;
3044
if exit_code != 0 {
3145
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
3246
}
@@ -59,6 +73,16 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
5973
return Err(DscError::NotImplemented("set".to_string()));
6074
};
6175
verify_json(resource, cwd, desired)?;
76+
77+
let mut env: Option<HashMap<String, String>> = None;
78+
let mut input_desired: Option<&str> = None;
79+
if set.input == InputKind::Env {
80+
env = Some(json_to_hashmap(desired)?);
81+
}
82+
else {
83+
input_desired = Some(desired);
84+
}
85+
6286
// if resource doesn't implement a pre-test, we execute test first to see if a set is needed
6387
if !skip_test && !set.pre_test.unwrap_or_default() {
6488
let test_result = invoke_test(resource, cwd, desired)?;
@@ -70,14 +94,34 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
7094
});
7195
}
7296
}
73-
let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), Some(desired), Some(cwd))?;
97+
98+
let mut get_env: Option<HashMap<String, String>> = None;
99+
let mut get_input: Option<&str> = None;
100+
match &resource.get.input {
101+
Some(InputKind::Env) => {
102+
get_env = Some(json_to_hashmap(desired)?);
103+
},
104+
Some(InputKind::Stdin) => {
105+
get_input = Some(desired);
106+
},
107+
None => {
108+
// leave input as none
109+
},
110+
}
111+
112+
let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), get_input, Some(cwd), get_env)?;
113+
if exit_code != 0 {
114+
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
115+
}
116+
74117
let pre_state: Value = if exit_code == 0 {
75118
serde_json::from_str(&stdout)?
76119
}
77120
else {
78121
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
79122
};
80-
let (exit_code, stdout, stderr) = invoke_command(&set.executable, set.args.clone(), Some(desired), Some(cwd))?;
123+
124+
let (exit_code, stdout, stderr) = invoke_command(&set.executable, set.args.clone(), input_desired, Some(cwd), env)?;
81125
if exit_code != 0 {
82126
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
83127
}
@@ -147,7 +191,17 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
147191
};
148192

149193
verify_json(resource, cwd, expected)?;
150-
let (exit_code, stdout, stderr) = invoke_command(&test.executable, test.args.clone(), Some(expected), Some(cwd))?;
194+
195+
let mut env: Option<HashMap<String, String>> = None;
196+
let mut input_expected: Option<&str> = None;
197+
if test.input == InputKind::Env {
198+
env = Some(json_to_hashmap(expected)?);
199+
}
200+
else {
201+
input_expected = Some(expected);
202+
}
203+
204+
let (exit_code, stdout, stderr) = invoke_command(&test.executable, test.args.clone(), input_expected, Some(cwd), env)?;
151205
if exit_code != 0 {
152206
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
153207
}
@@ -222,7 +276,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) ->
222276
return Err(DscError::NotImplemented("validate".to_string()));
223277
};
224278

225-
let (exit_code, stdout, stderr) = invoke_command(&validate.executable, validate.args.clone(), Some(config), Some(cwd))?;
279+
let (exit_code, stdout, stderr) = invoke_command(&validate.executable, validate.args.clone(), Some(config), Some(cwd), None)?;
226280
if exit_code != 0 {
227281
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
228282
}
@@ -247,7 +301,7 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result<String, DscE
247301

248302
match schema_kind {
249303
SchemaKind::Command(ref command) => {
250-
let (exit_code, stdout, stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd))?;
304+
let (exit_code, stdout, stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None)?;
251305
if exit_code != 0 {
252306
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
253307
}
@@ -291,7 +345,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str) -> Result<ExportRes
291345
return Err(DscError::Operation(format!("Export is not supported by resource {}", &resource.resource_type)))
292346
};
293347

294-
let (exit_code, stdout, stderr) = invoke_command(&export.executable, export.args.clone(), None, Some(cwd))?;
348+
let (exit_code, stdout, stderr) = invoke_command(&export.executable, export.args.clone(), None, Some(cwd), None)?;
295349
if exit_code != 0 {
296350
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
297351
}
@@ -324,7 +378,8 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str) -> Result<ExportRes
324378
/// # Errors
325379
///
326380
/// Error is returned if the command fails to execute or stdin/stdout/stderr cannot be opened.
327-
pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option<&str>, cwd: Option<&str>) -> Result<(i32, String, String), DscError> {
381+
#[allow(clippy::implicit_hasher)]
382+
pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option<&str>, cwd: Option<&str>, env: Option<HashMap<String, String>>) -> Result<(i32, String, String), DscError> {
328383
let mut command = Command::new(executable);
329384
if input.is_some() {
330385
command.stdin(Stdio::piped());
@@ -337,6 +392,9 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
337392
if let Some(cwd) = cwd {
338393
command.current_dir(cwd);
339394
}
395+
if let Some(env) = env {
396+
command.envs(env);
397+
}
340398

341399
let mut child = command.spawn()?;
342400
if input.is_some() {
@@ -399,3 +457,48 @@ fn verify_json(resource: &ResourceManifest, cwd: &str, json: &str) -> Result<(),
399457
};
400458
result
401459
}
460+
461+
fn json_to_hashmap(json: &str) -> Result<HashMap<String, String>, DscError> {
462+
let mut map = HashMap::new();
463+
let json: Value = serde_json::from_str(json)?;
464+
if let Value::Object(obj) = json {
465+
for (key, value) in obj {
466+
match value {
467+
Value::String(s) => {
468+
map.insert(key, s);
469+
},
470+
Value::Bool(b) => {
471+
map.insert(key, b.to_string());
472+
},
473+
Value::Number(n) => {
474+
map.insert(key, n.to_string());
475+
},
476+
Value::Array(a) => {
477+
// only array of number or strings is supported
478+
let mut array = Vec::new();
479+
for v in a {
480+
match v {
481+
Value::String(s) => {
482+
array.push(s);
483+
},
484+
Value::Number(n) => {
485+
array.push(n.to_string());
486+
},
487+
_ => {
488+
return Err(DscError::Operation(format!("Unsupported array value for key {key}. Only string and number is supported.")));
489+
},
490+
}
491+
}
492+
map.insert(key, array.join(","));
493+
},
494+
Value::Null => {
495+
continue;
496+
}
497+
Value::Object(_) => {
498+
return Err(DscError::Operation(format!("Unsupported value for key {key}. Only string, bool, number, and array is supported.")));
499+
},
500+
}
501+
}
502+
}
503+
Ok(map)
504+
}

dsc_lib/src/dscresources/resource_manifest.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ pub struct ResourceManifest {
5050

5151
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
5252
pub enum InputKind {
53-
/// The input is accepted as named parameters.
54-
#[serde(rename = "args")]
55-
Args,
53+
/// The input is accepted as environmental variables.
54+
#[serde(rename = "env")]
55+
Env,
5656
/// The input is accepted as a JSON object via STDIN.
5757
#[serde(rename = "stdin")]
5858
Stdin,

osinfo/tests/osinfo.tests.ps1

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ Describe 'osinfo resource tests' {
2525
}
2626

2727
It 'should perform synthetic test' {
28-
$out = '{"family": "does_not_exist"}' | dsc resource test -r '*osinfo' | ConvertFrom-Json
28+
if ($IsWindows) {
29+
$invalid = 'Linux'
30+
}
31+
else {
32+
$invalid = 'Windows'
33+
}
34+
$out = "{`"family`": `"$invalid`"}" | dsc resource test -r '*osinfo' | ConvertFrom-Json
2935
$actual = dsc resource get -r Microsoft/OSInfo | ConvertFrom-Json
3036
$out.actualState.family | Should -BeExactly $actual.actualState.family
3137
$out.actualState.version | Should -BeExactly $actual.actualState.version

powershellgroup/powershellgroup.dsc.resource.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"-NoProfile",
2828
"-Command",
2929
"$Input | ./powershellgroup.resource.ps1 Get"
30-
]
30+
],
31+
"input": "stdin"
3132
},
3233
"set": {
3334
"executable": "pwsh",

0 commit comments

Comments
 (0)