Skip to content

Commit

Permalink
Merge pull request #321 from tgauth/add-run-command-resource
Browse files Browse the repository at this point in the history
Add run command resource
  • Loading branch information
SteveL-MSFT committed Mar 5, 2024
2 parents 69d5601 + 32effa7 commit f345fef
Show file tree
Hide file tree
Showing 9 changed files with 525 additions and 1 deletion.
3 changes: 2 additions & 1 deletion build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ $projects = @(
"tools/test_group_resource",
"y2j",
"wmi-adapter",
"resources/brew"
"resources/brew",
"runcommandonset"
)
$pedantic_unclean_projects = @("ntreg")
$clippy_unclean_projects = @("tree-sitter-dscexpression")
Expand Down
14 changes: 14 additions & 0 deletions runcommandonset/Cargo.toml
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"] }
46 changes: 46 additions & 0 deletions runcommandonset/RunCommandOnSet.dsc.resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$schema": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/08/bundled/resource/manifest.json",
"description": "Takes a single-command line to execute on DSC set operation",
"type": "Microsoft.DSC.Transitional/RunCommandOnSet",
"version": "0.1.0",
"get": {
"executable": "runcommandonset",
"args": [
"get"
],
"input": "stdin"
},
"set": {
"executable": "runcommandonset",
"args": [
"set"
],
"input": "stdin",
"return": "state"
},
"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"
},
"exitCode": {
"title": "The expected exit code to indicate success, if non-zero. Default is zero for success.",
"type": "integer"
}
},
"additionalProperties": false
}
}
}
54 changes: 54 additions & 0 deletions runcommandonset/src/args.rs
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,
}
}
62 changes: 62 additions & 0 deletions runcommandonset/src/main.rs
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());
}
31 changes: 31 additions & 0 deletions runcommandonset/src/runcommand.rs
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(rename = "exitCode", default, skip_serializing_if = "is_default")]
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()
}

0 comments on commit f345fef

Please sign in to comment.