-
Notifications
You must be signed in to change notification settings - Fork 52
Add run command resource #321
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
Merged
Merged
Changes from 14 commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
b3b298e
start RunCommandOnSet resource
tgauth 026bc72
implement get and set
tgauth 7006d64
add resource manifest
tgauth c6ad454
add tests and update return format for get and set
tgauth 4d4a340
add tests and add proj to build list
tgauth 04d6893
add tests for missing required input
tgauth b0967b6
Merge branch 'main' into add-run-command-resource
tgauth fd9611b
fix clippy
tgauth a86655b
Merge branch 'add-run-command-resource' of https://github.com/tgauth/…
tgauth 0d43c39
fix tests on linux/macos
tgauth 25b6e1b
fix spacing
tgauth b417409
fix tests on linux/macos part 2
tgauth 5d62a3b
Merge branch 'main' into add-run-command-resource
tgauth 849b1d0
rename resource and remove yaml processing from utils
tgauth de6f29d
Merge branch 'main' into add-run-command-resource
tgauth 3f9ccc8
address review feedback
tgauth ce48d66
fix test
tgauth 2449eef
Merge branch 'main' into add-run-command-resource
tgauth 046001b
print debug message from other os
tgauth f5c9d39
Merge branch 'add-run-command-resource' of https://github.com/tgauth/…
tgauth 7ac34ca
fix test
tgauth 6d5c64b
remove debug from tests
tgauth aef1b19
fix merge conflict
tgauth 8401c66
add return type as state for set
tgauth 97a58f6
check changed properties for exit code in tests
tgauth 62284cf
Merge branch 'main' into add-run-command-resource
tgauth 32effa7
update exit_code to exitCode for serialization
tgauth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| [package] | ||
| name = "runcommandonset" | ||
| version = "0.1.0" | ||
| edition = "2021" | ||
|
|
||
| # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
|
||
| [dependencies] | ||
| atty = { version = "0.2" } | ||
| clap = { version = "4.4", features = ["derive"] } | ||
| serde = { version = "1.0", features = ["derive"] } | ||
| serde_json = { version = "1.0", features = ["preserve_order"] } | ||
| tracing = { version = "0.1.37" } | ||
| tracing-subscriber = { version = "0.3.17", features = ["ansi", "env-filter", "json"] } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| { | ||
| "$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json", | ||
| "description": "Takes a single-command line to execute on set", | ||
| "type": "Microsoft.DSC.Transitional/RunCommandOnSet", | ||
| "version": "0.1.0", | ||
| "get": { | ||
| "executable": "runcommandonset", | ||
| "args": [ | ||
| "get" | ||
| ], | ||
| "input": "stdin" | ||
| }, | ||
| "set": { | ||
| "executable": "runcommandonset", | ||
| "args": [ | ||
| "set" | ||
| ], | ||
| "input": "stdin" | ||
| }, | ||
| "schema": { | ||
| "embedded": { | ||
| "$schema": "http://json-schema.org/draft-07/schema#", | ||
| "title": "RunCommandOnSet", | ||
| "type": "object", | ||
| "required": [ | ||
| "executable" | ||
| ], | ||
| "properties": { | ||
| "arguments": { | ||
| "title": "The argument(s), if any, to pass to the executable that runs on set", | ||
| "type": "array" | ||
| }, | ||
| "executable": { | ||
| "title": "The executable to run on set", | ||
| "type": "string" | ||
| }, | ||
| "exit_code": { | ||
| "title": "The expected exit code to indicate success, if non-zero", | ||
tgauth marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "type": "integer" | ||
| } | ||
| }, | ||
| "additionalProperties": false | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use clap::{Parser, Subcommand, ValueEnum}; | ||
|
|
||
| #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] | ||
| pub enum TraceFormat { | ||
| Default, | ||
| Plaintext, | ||
| Json, | ||
| } | ||
|
|
||
| #[derive(Debug, Clone, PartialEq, Eq, ValueEnum)] | ||
| pub enum TraceLevel { | ||
| Error, | ||
| Warning, | ||
| Info, | ||
| Debug, | ||
| Trace | ||
| } | ||
|
|
||
| #[derive(Parser)] | ||
| #[clap(name = "runcommandonset", version = "0.0.1", about = "Run a command on set", long_about = None)] | ||
| pub struct Arguments { | ||
|
|
||
| #[clap(subcommand)] | ||
| pub subcommand: SubCommand, | ||
| #[clap(short = 'l', long, help = "Trace level to use", value_enum, default_value = "info")] | ||
| pub trace_level: TraceLevel, | ||
| #[clap(short = 'f', long, help = "Trace format to use", value_enum, default_value = "json")] | ||
| pub trace_format: TraceFormat, | ||
| } | ||
|
|
||
| #[derive(Debug, PartialEq, Eq, Subcommand)] | ||
| pub enum SubCommand { | ||
| #[clap(name = "get", about = "Get formatted command to run on set.")] | ||
| Get { | ||
| #[clap(short = 'a', long, help = "The arguments to pass to the executable.")] | ||
| arguments: Option<Vec<String>>, | ||
| #[clap(short = 'e', long, help = "The executable to run.")] | ||
| executable: Option<String>, | ||
| #[clap(short = 'c', long, help = "The expected exit code to indicate success, if non-zero.", default_value = "0")] | ||
| exit_code: i32, | ||
| }, | ||
| #[clap(name = "set", about = "Run formatted command.")] | ||
| Set { | ||
| #[clap(short = 'a', long, help = "The arguments to pass to the executable.")] | ||
| arguments: Option<Vec<String>>, | ||
| #[clap(short = 'e', long, help = "The executable to run.")] | ||
| executable: Option<String>, | ||
| #[clap(short = 'c', long, help = "The expected exit code to indicate success, if non-zero.", default_value = "0")] | ||
| exit_code: i32, | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use atty::Stream; | ||
| use clap::{Parser}; | ||
| use std::{io::{self, Read}, process::exit}; | ||
| use tracing::{error, warn, debug}; | ||
|
|
||
| use args::{Arguments, SubCommand}; | ||
| use runcommand::{RunCommand}; | ||
| use utils::{enable_tracing, invoke_command, parse_input, EXIT_INVALID_ARGS}; | ||
|
|
||
| pub mod args; | ||
| pub mod runcommand; | ||
| pub mod utils; | ||
|
|
||
| fn main() { | ||
| let args = Arguments::parse(); | ||
| enable_tracing(&args.trace_level, &args.trace_format); | ||
| warn!("This resource is not idempotent"); | ||
|
|
||
| let stdin = if atty::is(Stream::Stdin) { | ||
| None | ||
| } else { | ||
| debug!("Reading input from STDIN"); | ||
| let mut buffer: Vec<u8> = Vec::new(); | ||
| io::stdin().read_to_end(&mut buffer).unwrap(); | ||
| let stdin = match String::from_utf8(buffer) { | ||
| Ok(stdin) => stdin, | ||
| Err(e) => { | ||
| error!("Invalid UTF-8 sequence: {e}"); | ||
| exit(EXIT_INVALID_ARGS); | ||
| }, | ||
| }; | ||
| // parse_input expects at most 1 input, so wrapping Some(empty input) would throw it off | ||
| if stdin.is_empty() { | ||
| debug!("Input from STDIN is empty"); | ||
| None | ||
| } | ||
| else { | ||
| Some(stdin) | ||
| } | ||
| }; | ||
|
|
||
| let mut command: RunCommand; | ||
|
|
||
| match args.subcommand { | ||
| SubCommand::Get { arguments, executable, exit_code } => { | ||
| command = parse_input(arguments, executable, exit_code, stdin); | ||
| } | ||
| SubCommand::Set { arguments, executable, exit_code } => { | ||
| command = parse_input(arguments, executable, exit_code, stdin); | ||
| let (exit_code, stdout, stderr) = invoke_command(command.executable.as_ref(), command.arguments.clone()); | ||
| // TODO: convert this to tracing json once other PR is merged to handle tracing from resources | ||
| eprintln!("Stdout: {stdout}"); | ||
| eprintln!("Stderr: {stderr}"); | ||
| command.exit_code = exit_code; | ||
| } | ||
| } | ||
|
|
||
| println!("{}", command.to_json()); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use serde::{Deserialize, Serialize}; | ||
|
|
||
| #[derive(Debug, Deserialize, Clone, PartialEq, Serialize)] | ||
| pub struct RunCommand { | ||
| pub executable: String, | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub arguments: Option<Vec<String>>, | ||
| // default value for exit code is 0 | ||
| #[serde(default, skip_serializing_if = "is_default")] | ||
SteveL-MSFT marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| pub exit_code: i32, | ||
| } | ||
|
|
||
| impl RunCommand { | ||
| #[must_use] | ||
| pub fn to_json(&self) -> String { | ||
| match serde_json::to_string(self) { | ||
| Ok(json) => json, | ||
| Err(e) => { | ||
| eprintln!("Failed to serialize to JSON: {e}"); | ||
| String::new() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn is_default<T: Default + PartialEq>(t: &T) -> bool { | ||
| t == &T::default() | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use std::{io::Read, process::{Command, exit, Stdio}}; | ||
| use tracing::{Level, error, debug}; | ||
| use tracing_subscriber::{filter::EnvFilter, layer::SubscriberExt, Layer}; | ||
|
|
||
| use crate::args::{TraceFormat, TraceLevel}; | ||
| use crate::runcommand; | ||
|
|
||
| pub const EXIT_INVALID_ARGS: i32 = 1; | ||
| pub const EXIT_DSC_ERROR: i32 = 2; | ||
| pub const EXIT_CODE_MISMATCH: i32 = 3; | ||
| pub const EXIT_INVALID_INPUT: i32 = 4; | ||
| pub const EXIT_PROCESS_TERMINATED: i32 = 5; | ||
|
|
||
| /// Initialize `RunCommand` struct from input provided via stdin or via CLI arguments. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `arguments` - Optional arguments to pass to the command | ||
| /// * `executable` - The command to execute | ||
| /// * `exit_code` - The expected exit code upon success, if non-zero | ||
| /// * `stdin` - Optional JSON or YAML input provided via stdin | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Error message then exit if the `RunCommand` struct cannot be initialized from the provided inputs. | ||
| pub fn parse_input(arguments: Option<Vec<String>>, executable: Option<String>, exit_code: i32, stdin: Option<String>) -> runcommand::RunCommand { | ||
| let command: runcommand::RunCommand; | ||
| if let Some(input) = stdin { | ||
| debug!("Input: {}", input); | ||
| command = match serde_json::from_str(&input) { | ||
| Ok(json) => json, | ||
| Err(err) => { | ||
| error!("Error: Input is not valid: {err}"); | ||
| exit(EXIT_INVALID_INPUT); | ||
| } | ||
| } | ||
| } else if let Some(executable) = executable { | ||
| command = runcommand::RunCommand { | ||
| executable, | ||
| arguments, | ||
| exit_code, | ||
| }; | ||
| } | ||
| else { | ||
| error!("Error: Executable is required when input is not provided via stdin"); | ||
| exit(EXIT_INVALID_INPUT); | ||
| } | ||
| command | ||
| } | ||
|
|
||
| /// Setup tracing subscriber based on the provided trace level and format. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `trace_level` - The level of information of to output | ||
| /// * `trace_format` - The format of the output | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// If unable to initialize the tracing subscriber, an error message is printed and tracing is disabled. | ||
| pub fn enable_tracing(trace_level: &TraceLevel, trace_format: &TraceFormat) { | ||
| // originally implemented in dsc/src/util.rs | ||
| let tracing_level = match trace_level { | ||
| TraceLevel::Error => Level::ERROR, | ||
| TraceLevel::Warning => Level::WARN, | ||
| TraceLevel::Info => Level::INFO, | ||
| TraceLevel::Debug => Level::DEBUG, | ||
| TraceLevel::Trace => Level::TRACE, | ||
| }; | ||
|
|
||
| let filter = EnvFilter::try_from_default_env() | ||
| .or_else(|_| EnvFilter::try_new("warning")) | ||
| .unwrap_or_default() | ||
| .add_directive(tracing_level.into()); | ||
| let layer = tracing_subscriber::fmt::Layer::default().with_writer(std::io::stderr); | ||
| let fmt = match trace_format { | ||
| TraceFormat::Default => { | ||
| layer | ||
| .with_ansi(true) | ||
| .with_level(true) | ||
| .with_line_number(true) | ||
| .boxed() | ||
| }, | ||
| TraceFormat::Plaintext => { | ||
| layer | ||
| .with_ansi(false) | ||
| .with_level(true) | ||
| .with_line_number(false) | ||
| .boxed() | ||
| }, | ||
| TraceFormat::Json => { | ||
| layer | ||
| .with_ansi(false) | ||
| .with_level(true) | ||
| .with_line_number(true) | ||
| .json() | ||
| .boxed() | ||
| } | ||
| }; | ||
|
|
||
| let subscriber = tracing_subscriber::Registry::default().with(fmt).with(filter); | ||
|
|
||
| if tracing::subscriber::set_global_default(subscriber).is_err() { | ||
| eprintln!("Unable to set global default tracing subscriber. Tracing is diabled."); | ||
| } | ||
| } | ||
|
|
||
| /// Invoke a command and return the exit code, stdout, and stderr. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `executable` - The command to execute | ||
| /// * `args` - Optional arguments to pass to the command | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Error message then exit if the command fails to execute or stdin/stdout/stderr cannot be opened. | ||
| pub fn invoke_command(executable: &str, args: Option<Vec<String>>) -> (i32, String, String) { | ||
| // originally implemented in dsc_lib/src/dscresources/command_resource.rs | ||
| debug!("Invoking command {} with args {:?}", executable, args); | ||
tgauth marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let mut command = Command::new(executable); | ||
|
|
||
| command.stdout(Stdio::piped()); | ||
| command.stderr(Stdio::piped()); | ||
| if let Some(args) = args { | ||
| command.args(args); | ||
| } | ||
|
|
||
| let mut child = match command.spawn() { | ||
| Ok(child) => child, | ||
| Err(e) => { | ||
| error!("Failed to execute {}: {e}", executable); | ||
| exit(EXIT_DSC_ERROR); | ||
| } | ||
| }; | ||
|
|
||
| let Some(mut child_stdout) = child.stdout.take() else { | ||
| error!("Failed to open stdout for {}", executable); | ||
| exit(EXIT_DSC_ERROR); | ||
| }; | ||
| let mut stdout_buf = Vec::new(); | ||
| match child_stdout.read_to_end(&mut stdout_buf) { | ||
| Ok(_) => (), | ||
| Err(e) => { | ||
| error!("Failed to read stdout for {}: {e}", executable); | ||
| exit(EXIT_DSC_ERROR); | ||
| } | ||
| } | ||
|
|
||
| let Some(mut child_stderr) = child.stderr.take() else { | ||
| error!("Failed to open stderr for {}", executable); | ||
| exit(EXIT_DSC_ERROR); | ||
| }; | ||
| let mut stderr_buf = Vec::new(); | ||
| match child_stderr.read_to_end(&mut stderr_buf) { | ||
| Ok(_) => (), | ||
| Err(e) => { | ||
| error!("Failed to read stderr for {}: {e}", executable); | ||
| exit(EXIT_DSC_ERROR); | ||
| } | ||
| } | ||
|
|
||
| let exit_status = match child.wait() { | ||
| Ok(exit_status) => exit_status, | ||
| Err(e) => { | ||
| error!("Failed to wait for {}: {e}", executable); | ||
| exit(EXIT_DSC_ERROR); | ||
| } | ||
| }; | ||
|
|
||
| let exit_code = exit_status.code().unwrap_or(EXIT_PROCESS_TERMINATED); | ||
| let stdout = String::from_utf8_lossy(&stdout_buf).to_string(); | ||
| let stderr = String::from_utf8_lossy(&stderr_buf).to_string(); | ||
| (exit_code, stdout, stderr) | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.