Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support to pass JSON as env var #198

Merged
merged 3 commits into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions dsc/tests/dsc_resource_input.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'tests for resource input' {
BeforeAll {
$manifest = @'
{
"manifestVersion": "1.0.0",
"type": "Test/EnvVarInput",
"version": "0.1.0",
"get": {
"executable": "pwsh",
"input": "env",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-Command",
"\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\""
]
},
"set": {
"executable": "pwsh",
"input": "env",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-Command",
"\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\""
],
"return": "state",
"implementsPretest": true
},
"test": {
"executable": "pwsh",
"input": "env",
"args": [
"-NoLogo",
"-NonInteractive",
"-NoProfile",
"-Command",
"\"{ `\"Hello`\": `\"$env:Hello`\", `\"World`\": `\"$env:World`\", `\"Boolean`\": `\"$env:Boolean`\", `\"StringArray`\": `\"$env:StringArray`\", `\"NumberArray`\": `\"$env:NumberArray`\" }\""
]
},
"schema": {
"embedded": {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://test",
"title": "test",
"description": "test",
"type": "object",
"required": [],
"additionalProperties": false,
"properties": {
"Hello": {
"type": "string",
"description": "test"
},
"World": {
"type": "number",
"description": "test"
},
"Boolean": {
"type": "boolean",
"description": "test"
},
"StringArray": {
"type": "array",
"description": "test",
"items": {
"type": "string"
}
},
"NumberArray": {
"type": "array",
"description": "test",
"items": {
"type": "number"
}
}
}
}
}
}
'@
$oldPath = $env:DSC_RESOURCE_PATH
$env:DSC_RESOURCE_PATH = $TestDrive
Set-Content $TestDrive/EnvVarInput.dsc.resource.json -Value $manifest
}

AfterAll {
$env:DSC_RESOURCE_PATH = $oldPath
}

It 'Input can be sent to the resource for: <operation>' -TestCases @(
@{ operation = 'get'; member = 'actualState' }
@{ operation = 'set'; member = 'afterState' }
@{ operation = 'test'; member = 'actualState' }
) {
param($operation, $member)

$json = @"
{
"Hello": "foo",
"World": 2,
"Boolean": true,
"StringArray": ["foo", "bar"],
"NumberArray": [1, 2, 3]
}
"@

$result = $json | dsc resource $operation -r Test/EnvVarInput | ConvertFrom-Json
$result.$member.Hello | Should -BeExactly 'foo'
$result.$member.World | Should -Be 2
$result.$member.Boolean | Should -Be 'true'
$result.$member.StringArray | Should -BeExactly 'foo,bar'
$result.$member.NumberArray | Should -BeExactly '1,2,3'
}
}
2 changes: 1 addition & 1 deletion dsc_lib/src/discovery/command_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ impl ResourceDiscovery for CommandDiscovery {
let manifest = serde_json::from_value::<ResourceManifest>(provider_resource.manifest.clone().unwrap())?;
// invoke the list command
let list_command = manifest.provider.unwrap().list;
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&provider_resource.directory))
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&provider_resource.directory), None)
{
Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr),
Err(e) => {
Expand Down
129 changes: 116 additions & 13 deletions dsc_lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@

use jsonschema::JSONSchema;
use serde_json::Value;
use std::{process::Command, io::{Write, Read}, process::Stdio};

use std::{collections::HashMap, process::Command, io::{Write, Read}, process::Stdio};
use crate::dscerror::DscError;
use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}};
use super::{dscresource::get_diff,resource_manifest::{ResourceManifest, InputKind, ReturnKind, SchemaKind}, invoke_result::{GetResult, SetResult, TestResult, ValidateResult, ExportResult}};

pub const EXIT_PROCESS_TERMINATED: i32 = 0x102;

Expand All @@ -21,12 +20,27 @@ pub const EXIT_PROCESS_TERMINATED: i32 = 0x102;
///
/// Error returned if the resource does not successfully get the current state
pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Result<GetResult, DscError> {
if !filter.is_empty() && resource.get.input.is_some() {
let input_kind = if let Some(input_kind) = &resource.get.input {
input_kind.clone()
}
else {
InputKind::Stdin
};

let mut env: Option<HashMap<String, String>> = None;
let mut input_filter: Option<&str> = None;
if !filter.is_empty() {
verify_json(resource, cwd, filter)?;

if input_kind == InputKind::Env {
env = Some(json_to_hashmap(filter)?);
}
else {
input_filter = Some(filter);
}
}

let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), Some(filter), Some(cwd))?;
//println!("{stdout}");
let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), input_filter, Some(cwd), env)?;
if exit_code != 0 {
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
}
Expand Down Expand Up @@ -59,6 +73,16 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
return Err(DscError::NotImplemented("set".to_string()));
};
verify_json(resource, cwd, desired)?;

let mut env: Option<HashMap<String, String>> = None;
let mut input_desired: Option<&str> = None;
if set.input == InputKind::Env {
env = Some(json_to_hashmap(desired)?);
}
else {
input_desired = Some(desired);
}

// 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.unwrap_or_default() {
let test_result = invoke_test(resource, cwd, desired)?;
Expand All @@ -70,14 +94,34 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
});
}
}
let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), Some(desired), Some(cwd))?;

let mut get_env: Option<HashMap<String, String>> = None;
let mut get_input: Option<&str> = None;
match &resource.get.input {
Some(InputKind::Env) => {
get_env = Some(json_to_hashmap(desired)?);
},
Some(InputKind::Stdin) => {
get_input = Some(desired);
},
None => {
// leave input as none
},
}

let (exit_code, stdout, stderr) = invoke_command(&resource.get.executable, resource.get.args.clone(), get_input, Some(cwd), get_env)?;
if exit_code != 0 {
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
}

let pre_state: Value = if exit_code == 0 {
serde_json::from_str(&stdout)?
}
else {
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
};
let (exit_code, stdout, stderr) = invoke_command(&set.executable, set.args.clone(), Some(desired), Some(cwd))?;

let (exit_code, stdout, stderr) = invoke_command(&set.executable, set.args.clone(), input_desired, Some(cwd), env)?;
if exit_code != 0 {
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
}
Expand Down Expand Up @@ -147,7 +191,17 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
};

verify_json(resource, cwd, expected)?;
let (exit_code, stdout, stderr) = invoke_command(&test.executable, test.args.clone(), Some(expected), Some(cwd))?;

let mut env: Option<HashMap<String, String>> = None;
let mut input_expected: Option<&str> = None;
if test.input == InputKind::Env {
env = Some(json_to_hashmap(expected)?);
}
else {
input_expected = Some(expected);
}

let (exit_code, stdout, stderr) = invoke_command(&test.executable, test.args.clone(), input_expected, Some(cwd), env)?;
if exit_code != 0 {
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
}
Expand Down Expand Up @@ -222,7 +276,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) ->
return Err(DscError::NotImplemented("validate".to_string()));
};

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

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

let (exit_code, stdout, stderr) = invoke_command(&export.executable, export.args.clone(), None, Some(cwd))?;
let (exit_code, stdout, stderr) = invoke_command(&export.executable, export.args.clone(), None, Some(cwd), None)?;
if exit_code != 0 {
return Err(DscError::Command(resource.resource_type.clone(), exit_code, stderr));
}
Expand Down Expand Up @@ -324,7 +378,8 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str) -> Result<ExportRes
/// # Errors
///
/// Error is returned if the command fails to execute or stdin/stdout/stderr cannot be opened.
pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option<&str>, cwd: Option<&str>) -> Result<(i32, String, String), DscError> {
#[allow(clippy::implicit_hasher)]
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> {
let mut command = Command::new(executable);
if input.is_some() {
command.stdin(Stdio::piped());
Expand All @@ -337,6 +392,9 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
if let Some(cwd) = cwd {
command.current_dir(cwd);
}
if let Some(env) = env {
command.envs(env);
}

let mut child = command.spawn()?;
if input.is_some() {
Expand Down Expand Up @@ -399,3 +457,48 @@ fn verify_json(resource: &ResourceManifest, cwd: &str, json: &str) -> Result<(),
};
result
}

fn json_to_hashmap(json: &str) -> Result<HashMap<String, String>, DscError> {
let mut map = HashMap::new();
let json: Value = serde_json::from_str(json)?;
if let Value::Object(obj) = json {
for (key, value) in obj {
match value {
Value::String(s) => {
map.insert(key, s);
},
Value::Bool(b) => {
map.insert(key, b.to_string());
},
Value::Number(n) => {
map.insert(key, n.to_string());
},
Value::Array(a) => {
// only array of number or strings is supported
let mut array = Vec::new();
for v in a {
match v {
Value::String(s) => {
array.push(s);
},
Value::Number(n) => {
array.push(n.to_string());
},
_ => {
return Err(DscError::Operation(format!("Unsupported array value for key {key}. Only string and number is supported.")));
},
}
}
map.insert(key, array.join(","));
},
Value::Null => {
continue;
}
Value::Object(_) => {
return Err(DscError::Operation(format!("Unsupported value for key {key}. Only string, bool, number, and array is supported.")));
},
}
}
}
Ok(map)
}
6 changes: 3 additions & 3 deletions dsc_lib/src/dscresources/resource_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ pub struct ResourceManifest {

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum InputKind {
/// The input is accepted as named parameters.
#[serde(rename = "args")]
Args,
/// The input is accepted as environmental variables.
#[serde(rename = "env")]
Env,
/// The input is accepted as a JSON object via STDIN.
#[serde(rename = "stdin")]
Stdin,
Expand Down
8 changes: 7 additions & 1 deletion osinfo/tests/osinfo.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ Describe 'osinfo resource tests' {
}

It 'should perform synthetic test' {
$out = '{"family": "does_not_exist"}' | dsc resource test -r '*osinfo' | ConvertFrom-Json
if ($IsWindows) {
$invalid = 'Linux'
}
else {
$invalid = 'Windows'
}
$out = "{`"family`": `"$invalid`"}" | dsc resource test -r '*osinfo' | ConvertFrom-Json
$actual = dsc resource get -r Microsoft/OSInfo | ConvertFrom-Json
$out.actualState.family | Should -BeExactly $actual.actualState.family
$out.actualState.version | Should -BeExactly $actual.actualState.version
Expand Down
3 changes: 2 additions & 1 deletion powershellgroup/powershellgroup.dsc.resource.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"-NoProfile",
"-Command",
"$Input | ./powershellgroup.resource.ps1 Get"
]
],
"input": "stdin"
},
"set": {
"executable": "pwsh",
Expand Down