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

retrieve exit code info from manifest if available #444

Merged
merged 5 commits into from
Jun 14, 2024
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
18 changes: 18 additions & 0 deletions dsc/tests/dsc.exit_code.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'exit code tests' {
It 'non-zero exit code in manifest has corresponding message' {
$result = dsc resource get -r Test/ExitCode --input "{ exitCode: 8 }" 2>&1
$result | Should -Match 'ERROR.*?[Exit code 8].*?manifest description: Placeholder from manifest for exit code 8'
}
It 'non-zero exit code not in manifest has generic message' {
$result = dsc resource get -r Test/ExitCode --input "{ exitCode: 1 }" 2>&1
$result | Should -Match 'ERROR.*?Error.*?[Exit code 1]'
}
It 'success exit code executes without error' {
$result = dsc resource get -r Test/ExitCode --input "{ exitCode: 0 }" | ConvertFrom-Json
$result.actualState.exitCode | Should -Be 0
$LASTEXITCODE | Should -Be 0
}
}
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 @@ -221,7 +221,7 @@ impl ResourceDiscovery for CommandDiscovery {
let mut adapter_resources_count = 0;
// invoke the list command
let list_command = manifest.adapter.unwrap().list;
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&adapter.directory), None)
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&adapter.directory), None, &manifest.exit_codes)
{
Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr),
Err(e) => {
Expand Down
3 changes: 3 additions & 0 deletions dsc_lib/src/dscerror.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub enum DscError {
#[error("Command: Executable '{0}' [Exit code {1}] {2}")]
CommandExit(String, i32, String),

#[error("Command: Resource '{0}' [Exit code {1}] manifest description: {2}")]
CommandExitFromManifest(String, i32, String),

#[error("CommandOperation: {0} for executable '{1}'")]
CommandOperation(String, String),

Expand Down
27 changes: 16 additions & 11 deletions dsc_lib/src/dscresources/command_resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul
}

info!("Invoking get '{}' using '{}'", &resource.resource_type, &get.executable);
let (_exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
let (_exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;
if resource.kind == Some(Kind::Resource) {
debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &get.executable);
verify_json(resource, cwd, &stdout)?;
Expand Down Expand Up @@ -156,8 +156,8 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
let args = process_args(&get.args, desired);
let command_input = get_command_input(&get.input, desired)?;

info!("Getting current state for {} by invoking get '{}' using '{}'", operation_type, &resource.resource_type, &get.executable);
let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
info!("Getting current state for set by invoking get '{}' using '{}'", &resource.resource_type, &get.executable);
let (exit_code, stdout, stderr) = invoke_command(&get.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;

if resource.kind == Some(Kind::Resource) {
debug!("Verifying output of get '{}' using '{}'", &resource.resource_type, &get.executable);
Expand Down Expand Up @@ -187,7 +187,7 @@ pub fn invoke_set(resource: &ResourceManifest, cwd: &str, desired: &str, skip_te
}

info!("Invoking {} '{}' using '{}'", operation_type, &resource.resource_type, &set.executable);
let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env)?;
let (exit_code, stdout, stderr) = invoke_command(&set.executable, args, input_desired, Some(cwd), env, &resource.exit_codes)?;

match set.returns {
Some(ReturnKind::State) => {
Expand Down Expand Up @@ -280,7 +280,7 @@ pub fn invoke_test(resource: &ResourceManifest, cwd: &str, expected: &str) -> Re
let command_input = get_command_input(&test.input, expected)?;

info!("Invoking test '{}' using '{}'", &resource.resource_type, &test.executable);
let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
let (exit_code, stdout, stderr) = invoke_command(&test.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;

if resource.kind == Some(Kind::Resource) {
debug!("Verifying output of test '{}' using '{}'", &resource.resource_type, &test.executable);
Expand Down Expand Up @@ -394,7 +394,7 @@ pub fn invoke_delete(resource: &ResourceManifest, cwd: &str, filter: &str) -> Re
let command_input = get_command_input(&delete.input, filter)?;

info!("Invoking delete '{}' using '{}'", &resource.resource_type, &delete.executable);
let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
let (_exit_code, _stdout, _stderr) = invoke_command(&delete.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;

Ok(())
}
Expand Down Expand Up @@ -425,7 +425,7 @@ pub fn invoke_validate(resource: &ResourceManifest, cwd: &str, config: &str) ->
let command_input = get_command_input(&validate.input, config)?;

info!("Invoking validate '{}' using '{}'", &resource.resource_type, &validate.executable);
let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;
let result: ValidateResult = serde_json::from_str(&stdout)?;
Ok(result)
}
Expand All @@ -446,7 +446,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), None)?;
let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None, &resource.exit_codes)?;
Ok(stdout)
},
SchemaKind::Embedded(ref schema) => {
Expand Down Expand Up @@ -501,7 +501,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str>
args = process_args(&export.args, "");
}

let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
let (_exit_code, stdout, stderr) = invoke_command(&export.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;
let mut instances: Vec<Value> = Vec::new();
for line in stdout.lines()
{
Expand Down Expand Up @@ -547,7 +547,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &str, input: &str) -> Re
let command_input = get_command_input(&resolve.input, input)?;

info!("Invoking resolve '{}' using '{}'", &resource.resource_type, &resolve.executable);
let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;
let result: ResolveResult = serde_json::from_str(&stdout)?;
Ok(result)
}
Expand All @@ -565,7 +565,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &str, input: &str) -> Re
///
/// Error is returned if the command fails to execute or stdin/stdout/stderr cannot be opened.
#[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> {
pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option<&str>, cwd: Option<&str>, env: Option<HashMap<String, String>>, exit_codes: &Option<HashMap<i32, String>>) -> Result<(i32, String, String), DscError> {
debug!("Invoking command '{}' with args {:?}", executable, args);
let mut command = Command::new(executable);
if input.is_some() {
Expand Down Expand Up @@ -629,6 +629,11 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
};

if exit_code != 0 {
if let Some(exit_codes) = exit_codes {
if let Some(error_message) = exit_codes.get(&exit_code) {
return Err(DscError::CommandExitFromManifest(executable.to_string(), exit_code, error_message.to_string()));
}
}
return Err(DscError::Command(executable.to_string(), exit_code, cleaned_stderr));
}

Expand Down
28 changes: 28 additions & 0 deletions tools/dsctest/dscexitcode.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
"type": "Test/ExitCode",
"version": "0.1.0",
"get": {
"executable": "dsctest",
"args": [
"exit-code",
{
"jsonInputArg": "--input"
}
]
},
"exitCodes": {
"0": "Success",
"8": "Placeholder from manifest for exit code 8"
},
"schema": {
"command": {
"executable": "dsctest",
"args": [
"schema",
"-s",
"exit-code"
]
}
}
}
7 changes: 7 additions & 0 deletions tools/dsctest/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum Schemas {
Delete,
Echo,
Exist,
ExitCode,
Sleep,
Trace,
WhatIf,
Expand Down Expand Up @@ -41,6 +42,12 @@ pub enum SubCommand {
input: String,
},

#[clap(name = "exit-code", about = "Return the exit code specified in the input")]
ExitCode {
#[clap(name = "input", short, long, help = "The input to the exit code command as JSON")]
input: String,
},

#[clap(name = "schema", about = "Get the JSON schema for a subcommand")]
Schema {
#[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")]
Expand Down
12 changes: 12 additions & 0 deletions tools/dsctest/src/exit_code.rs
Original file line number Diff line number Diff line change
@@ -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 ExitCode {
#[serde(rename = "exitCode")]
pub exit_code: i32,
}
20 changes: 20 additions & 0 deletions tools/dsctest/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod args;
mod delete;
mod echo;
mod exist;
mod exit_code;
mod sleep;
mod trace;
mod whatif;
Expand All @@ -15,11 +16,13 @@ use schemars::schema_for;
use crate::delete::Delete;
use crate::echo::Echo;
use crate::exist::{Exist, State};
use crate::exit_code::ExitCode;
use crate::sleep::Sleep;
use crate::trace::Trace;
use crate::whatif::WhatIf;
use std::{thread, time::Duration};

#[allow(clippy::too_many_lines)]
fn main() {
let args = Args::parse();
let json = match args.subcommand {
Expand Down Expand Up @@ -60,6 +63,20 @@ fn main() {

serde_json::to_string(&exist).unwrap()
},
SubCommand::ExitCode { input } => {
let exit_code = match serde_json::from_str::<ExitCode>(&input) {
Ok(exit_code) => exit_code,
Err(err) => {
eprintln!("Error JSON does not match schema: {err}");
std::process::exit(1);
}
};
if exit_code.exit_code != 0 {
eprintln!("Exiting with code: {}", exit_code.exit_code);
std::process::exit(exit_code.exit_code);
}
input
},
SubCommand::Schema { subcommand } => {
let schema = match subcommand {
Schemas::Delete => {
Expand All @@ -71,6 +88,9 @@ fn main() {
Schemas::Exist => {
schema_for!(Exist)
},
Schemas::ExitCode => {
schema_for!(ExitCode)
},
Schemas::Sleep => {
schema_for!(Sleep)
},
Expand Down
Loading