Skip to content

Commit a87ef22

Browse files
authored
Merge pull request #444 from tgauth/use-manifest-exit-code
retrieve exit code info from manifest if available
2 parents c739e44 + 7a18835 commit a87ef22

File tree

8 files changed

+105
-12
lines changed

8 files changed

+105
-12
lines changed

dsc/tests/dsc.exit_code.tests.ps1

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'exit code tests' {
5+
It 'non-zero exit code in manifest has corresponding message' {
6+
$result = dsc resource get -r Test/ExitCode --input "{ exitCode: 8 }" 2>&1
7+
$result | Should -Match 'ERROR.*?[Exit code 8].*?manifest description: Placeholder from manifest for exit code 8'
8+
}
9+
It 'non-zero exit code not in manifest has generic message' {
10+
$result = dsc resource get -r Test/ExitCode --input "{ exitCode: 1 }" 2>&1
11+
$result | Should -Match 'ERROR.*?Error.*?[Exit code 1]'
12+
}
13+
It 'success exit code executes without error' {
14+
$result = dsc resource get -r Test/ExitCode --input "{ exitCode: 0 }" | ConvertFrom-Json
15+
$result.actualState.exitCode | Should -Be 0
16+
$LASTEXITCODE | Should -Be 0
17+
}
18+
}

dsc_lib/src/discovery/command_discovery.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ impl ResourceDiscovery for CommandDiscovery {
221221
let mut adapter_resources_count = 0;
222222
// invoke the list command
223223
let list_command = manifest.adapter.unwrap().list;
224-
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&adapter.directory), None)
224+
let (exit_code, stdout, stderr) = match invoke_command(&list_command.executable, list_command.args, None, Some(&adapter.directory), None, &manifest.exit_codes)
225225
{
226226
Ok((exit_code, stdout, stderr)) => (exit_code, stdout, stderr),
227227
Err(e) => {

dsc_lib/src/dscerror.rs

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ pub enum DscError {
2020
#[error("Command: Executable '{0}' [Exit code {1}] {2}")]
2121
CommandExit(String, i32, String),
2222

23+
#[error("Command: Resource '{0}' [Exit code {1}] manifest description: {2}")]
24+
CommandExitFromManifest(String, i32, String),
25+
2326
#[error("CommandOperation: {0} for executable '{1}'")]
2427
CommandOperation(String, String),
2528

dsc_lib/src/dscresources/command_resource.rs

+16-11
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ pub fn invoke_get(resource: &ResourceManifest, cwd: &str, filter: &str) -> Resul
5757
}
5858

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

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

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

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

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

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

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

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

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

427427
info!("Invoking validate '{}' using '{}'", &resource.resource_type, &validate.executable);
428-
let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
428+
let (_exit_code, stdout, _stderr) = invoke_command(&validate.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;
429429
let result: ValidateResult = serde_json::from_str(&stdout)?;
430430
Ok(result)
431431
}
@@ -446,7 +446,7 @@ pub fn get_schema(resource: &ResourceManifest, cwd: &str) -> Result<String, DscE
446446

447447
match schema_kind {
448448
SchemaKind::Command(ref command) => {
449-
let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None)?;
449+
let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None, &resource.exit_codes)?;
450450
Ok(stdout)
451451
},
452452
SchemaKind::Embedded(ref schema) => {
@@ -501,7 +501,7 @@ pub fn invoke_export(resource: &ResourceManifest, cwd: &str, input: Option<&str>
501501
args = process_args(&export.args, "");
502502
}
503503

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

549549
info!("Invoking resolve '{}' using '{}'", &resource.resource_type, &resolve.executable);
550-
let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env)?;
550+
let (_exit_code, stdout, _stderr) = invoke_command(&resolve.executable, args, command_input.stdin.as_deref(), Some(cwd), command_input.env, &resource.exit_codes)?;
551551
let result: ResolveResult = serde_json::from_str(&stdout)?;
552552
Ok(result)
553553
}
@@ -565,7 +565,7 @@ pub fn invoke_resolve(resource: &ResourceManifest, cwd: &str, input: &str) -> Re
565565
///
566566
/// Error is returned if the command fails to execute or stdin/stdout/stderr cannot be opened.
567567
#[allow(clippy::implicit_hasher)]
568-
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> {
568+
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> {
569569
debug!("Invoking command '{}' with args {:?}", executable, args);
570570
let mut command = Command::new(executable);
571571
if input.is_some() {
@@ -629,6 +629,11 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
629629
};
630630

631631
if exit_code != 0 {
632+
if let Some(exit_codes) = exit_codes {
633+
if let Some(error_message) = exit_codes.get(&exit_code) {
634+
return Err(DscError::CommandExitFromManifest(executable.to_string(), exit_code, error_message.to_string()));
635+
}
636+
}
632637
return Err(DscError::Command(executable.to_string(), exit_code, cleaned_stderr));
633638
}
634639

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2024/04/bundled/resource/manifest.json",
3+
"type": "Test/ExitCode",
4+
"version": "0.1.0",
5+
"get": {
6+
"executable": "dsctest",
7+
"args": [
8+
"exit-code",
9+
{
10+
"jsonInputArg": "--input"
11+
}
12+
]
13+
},
14+
"exitCodes": {
15+
"0": "Success",
16+
"8": "Placeholder from manifest for exit code 8"
17+
},
18+
"schema": {
19+
"command": {
20+
"executable": "dsctest",
21+
"args": [
22+
"schema",
23+
"-s",
24+
"exit-code"
25+
]
26+
}
27+
}
28+
}

tools/dsctest/src/args.rs

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub enum Schemas {
88
Delete,
99
Echo,
1010
Exist,
11+
ExitCode,
1112
Sleep,
1213
Trace,
1314
WhatIf,
@@ -41,6 +42,12 @@ pub enum SubCommand {
4142
input: String,
4243
},
4344

45+
#[clap(name = "exit-code", about = "Return the exit code specified in the input")]
46+
ExitCode {
47+
#[clap(name = "input", short, long, help = "The input to the exit code command as JSON")]
48+
input: String,
49+
},
50+
4451
#[clap(name = "schema", about = "Get the JSON schema for a subcommand")]
4552
Schema {
4653
#[clap(name = "subcommand", short, long, help = "The subcommand to get the schema for")]

tools/dsctest/src/exit_code.rs

+12
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 ExitCode {
10+
#[serde(rename = "exitCode")]
11+
pub exit_code: i32,
12+
}

tools/dsctest/src/main.rs

+20
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod args;
55
mod delete;
66
mod echo;
77
mod exist;
8+
mod exit_code;
89
mod sleep;
910
mod trace;
1011
mod whatif;
@@ -15,11 +16,13 @@ use schemars::schema_for;
1516
use crate::delete::Delete;
1617
use crate::echo::Echo;
1718
use crate::exist::{Exist, State};
19+
use crate::exit_code::ExitCode;
1820
use crate::sleep::Sleep;
1921
use crate::trace::Trace;
2022
use crate::whatif::WhatIf;
2123
use std::{thread, time::Duration};
2224

25+
#[allow(clippy::too_many_lines)]
2326
fn main() {
2427
let args = Args::parse();
2528
let json = match args.subcommand {
@@ -60,6 +63,20 @@ fn main() {
6063

6164
serde_json::to_string(&exist).unwrap()
6265
},
66+
SubCommand::ExitCode { input } => {
67+
let exit_code = match serde_json::from_str::<ExitCode>(&input) {
68+
Ok(exit_code) => exit_code,
69+
Err(err) => {
70+
eprintln!("Error JSON does not match schema: {err}");
71+
std::process::exit(1);
72+
}
73+
};
74+
if exit_code.exit_code != 0 {
75+
eprintln!("Exiting with code: {}", exit_code.exit_code);
76+
std::process::exit(exit_code.exit_code);
77+
}
78+
input
79+
},
6380
SubCommand::Schema { subcommand } => {
6481
let schema = match subcommand {
6582
Schemas::Delete => {
@@ -71,6 +88,9 @@ fn main() {
7188
Schemas::Exist => {
7289
schema_for!(Exist)
7390
},
91+
Schemas::ExitCode => {
92+
schema_for!(ExitCode)
93+
},
7494
Schemas::Sleep => {
7595
schema_for!(Sleep)
7696
},

0 commit comments

Comments
 (0)