diff --git a/dsc/Cargo.lock b/dsc/Cargo.lock index e098aba9e..6af068d49 100644 --- a/dsc/Cargo.lock +++ b/dsc/Cargo.lock @@ -551,7 +551,7 @@ dependencies = [ [[package]] name = "dsc" -version = "3.1.1" +version = "3.1.2" dependencies = [ "clap", "clap_complete", diff --git a/dsc/Cargo.toml b/dsc/Cargo.toml index d5384b24d..469757db5 100644 --- a/dsc/Cargo.toml +++ b/dsc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dsc" -version = "3.1.1" +version = "3.1.2" edition = "2021" [profile.release] diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 37ad5a1ed..4b00ded6f 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -35,7 +35,8 @@ use std::{ collections::HashMap, io::{self, IsTerminal}, path::Path, - process::exit + process::exit, + slice::from_ref, }; use tracing::{debug, error, trace}; @@ -495,7 +496,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat) continue; } - resource_types.push(type_name.to_lowercase().to_string()); + resource_types.push(type_name.to_lowercase().clone()); } dsc.find_resources(&resource_types, progress_format); @@ -578,16 +579,16 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat list_resources(&mut dsc, resource_name.as_ref(), adapter_name.as_ref(), description.as_ref(), tags.as_ref(), output_format.as_ref(), progress_format); }, ResourceSubCommand::Schema { resource , output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + dsc.find_resources(from_ref(resource), progress_format); resource_command::schema(&dsc, resource, output_format.as_ref()); }, ResourceSubCommand::Export { resource, input, file, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + dsc.find_resources(from_ref(resource), progress_format); let parsed_input = get_input(input.as_ref(), file.as_ref(), false); resource_command::export(&mut dsc, resource, &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Get { resource, input, file: path, all, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + dsc.find_resources(from_ref(resource), progress_format); if *all { resource_command::get_all(&dsc, resource, output_format.as_ref()); } @@ -601,17 +602,17 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat } }, ResourceSubCommand::Set { resource, input, file: path, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + dsc.find_resources(from_ref(resource), progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref(), false); resource_command::set(&dsc, resource, &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Test { resource, input, file: path, output_format } => { - dsc.find_resources(&[resource.to_string()], progress_format); + dsc.find_resources(from_ref(resource), progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref(), false); resource_command::test(&dsc, resource, &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Delete { resource, input, file: path } => { - dsc.find_resources(&[resource.to_string()], progress_format); + dsc.find_resources(from_ref(resource), progress_format); let parsed_input = get_input(input.as_ref(), path.as_ref(), false); resource_command::delete(&dsc, resource, &parsed_input); }, diff --git a/dsc/tests/dsc_discovery.tests.ps1 b/dsc/tests/dsc_discovery.tests.ps1 index 6d377a3bc..d4489ec6f 100644 --- a/dsc/tests/dsc_discovery.tests.ps1 +++ b/dsc/tests/dsc_discovery.tests.ps1 @@ -251,4 +251,55 @@ Describe 'tests for resource discovery' { $env:DSC_RESOURCE_PATH = $oldPath } } + + It 'Resource manifest using relative path to exe: ' -TestCases @( + @{ path = '../dscecho'; success = $true } + @{ path = '../foo/dscecho'; success = $false } + ) { + param($path, $success) + $manifest = @" +{ + "`$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "type": "Microsoft.DSC.Debug/Echo", + "version": "1.0.0", + "description": "Echo resource for testing and debugging purposes", + "get": { + "executable": "$path", + "args": [ + { + "jsonInputArg": "--input", + "mandatory": true + } + ] + }, + "schema": { + "command": { + "executable": "$path" + } + } +} +"@ + $dscEcho = Get-Command dscecho -ErrorAction Stop + # copy to testdrive + Copy-Item -Path "$($dscEcho.Source)" -Destination $testdrive + # create manifest in subfolder + $subfolder = Join-Path $testdrive 'subfolder' + New-Item -Path $subfolder -ItemType Directory -Force | Out-Null + Set-Content -Path (Join-Path $subfolder 'test.dsc.resource.json') -Value $manifest + + try { + $env:DSC_RESOURCE_PATH = $subfolder + $out = dsc resource get -r 'Microsoft.DSC.Debug/Echo' -i '{"output":"RelativePathTest"}' 2> "$testdrive/error.txt" | ConvertFrom-Json + if ($success) { + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path "$testdrive/error.txt") + $out.actualState.output | Should -BeExactly 'RelativePathTest' + } else { + $LASTEXITCODE | Should -Be 2 -Because (Get-Content -Raw -Path "$testdrive/error.txt") + (Get-Content -Raw -Path "$testdrive/error.txt") | Should -Match "ERROR.*?Executable '\.\./foo/dscecho(\.exe)?' not found" + } + } + finally { + $env:DSC_RESOURCE_PATH = $null + } + } } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 2b89e7b89..92fe7f513 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -370,3 +370,5 @@ foundSetting = "Found setting '%{name}' in %{path}" notFoundSetting = "Setting '%{name}' not found in %{path}" failedToGetExePath = "Can't get 'dsc' executable path" settingNotFound = "Setting '%{name}' not found" +executableNotFoundInWorkingDirectory = "Executable '%{executable}' not found with working directory '%{cwd}'" +executableNotFound = "Executable '%{executable}' not found" diff --git a/dsc_lib/src/configure/mod.rs b/dsc_lib/src/configure/mod.rs index 3039e581a..7139b3780 100644 --- a/dsc_lib/src/configure/mod.rs +++ b/dsc_lib/src/configure/mod.rs @@ -703,7 +703,7 @@ impl Configurator { value.clone() }; info!("{}", t!("configure.mod.setVariable", name = name, value = new_value)); - self.context.variables.insert(name.to_string(), new_value); + self.context.variables.insert(name.clone(), new_value); } Ok(()) } @@ -839,13 +839,13 @@ fn get_failure_from_error(err: &DscError) -> Option { match err { DscError::CommandExit(_resource, exit_code, reason) => { Some(Failure { - message: reason.to_string(), + message: reason.clone(), exit_code: *exit_code, }) }, DscError::CommandExitFromManifest(_resource, exit_code, reason) => { Some(Failure { - message: reason.to_string(), + message: reason.clone(), exit_code: *exit_code, }) }, diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index 61bacc2b6..2a8208b40 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -22,10 +22,10 @@ use std::fs; use std::path::{Path, PathBuf}; use std::str::FromStr; use tracing::{debug, info, trace, warn}; -use which::which; use crate::util::get_setting; use crate::util::get_exe_path; +use crate::util::canonicalize_which; const DSC_RESOURCE_EXTENSIONS: [&str; 3] = [".dsc.resource.json", ".dsc.resource.yaml", ".dsc.resource.yml"]; const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extension.yaml", ".dsc.extension.yml"]; @@ -121,10 +121,11 @@ impl CommandDiscovery { let dsc_resource_path = env::var_os("DSC_RESOURCE_PATH"); if resource_path_setting.allow_env_override && dsc_resource_path.is_some(){ - let value = dsc_resource_path.unwrap(); - debug!("DSC_RESOURCE_PATH: {:?}", value.to_string_lossy()); - using_custom_path = true; - paths.append(&mut env::split_paths(&value).collect::>()); + if let Some(value) = dsc_resource_path { + debug!("DSC_RESOURCE_PATH: {:?}", value.to_string_lossy()); + using_custom_path = true; + paths.append(&mut env::split_paths(&value).collect::>()); + } } else { for p in resource_path_setting.directories { let v = PathBuf::from_str(&p); @@ -649,38 +650,38 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result = vec![]; if let Some(get) = &manifest.get { - verify_executable(&manifest.resource_type, "get", &get.executable); + verify_executable(&manifest.resource_type, "get", &get.executable, path.parent().unwrap()); capabilities.push(Capability::Get); } if let Some(set) = &manifest.set { - verify_executable(&manifest.resource_type, "set", &set.executable); + verify_executable(&manifest.resource_type, "set", &set.executable, path.parent().unwrap()); capabilities.push(Capability::Set); if set.handles_exist == Some(true) { capabilities.push(Capability::SetHandlesExist); } } if let Some(what_if) = &manifest.what_if { - verify_executable(&manifest.resource_type, "what_if", &what_if.executable); + verify_executable(&manifest.resource_type, "what_if", &what_if.executable, path.parent().unwrap()); capabilities.push(Capability::WhatIf); } if let Some(test) = &manifest.test { - verify_executable(&manifest.resource_type, "test", &test.executable); + verify_executable(&manifest.resource_type, "test", &test.executable, path.parent().unwrap()); capabilities.push(Capability::Test); } if let Some(delete) = &manifest.delete { - verify_executable(&manifest.resource_type, "delete", &delete.executable); + verify_executable(&manifest.resource_type, "delete", &delete.executable, path.parent().unwrap()); capabilities.push(Capability::Delete); } if let Some(export) = &manifest.export { - verify_executable(&manifest.resource_type, "export", &export.executable); + verify_executable(&manifest.resource_type, "export", &export.executable, path.parent().unwrap()); capabilities.push(Capability::Export); } if let Some(resolve) = &manifest.resolve { - verify_executable(&manifest.resource_type, "resolve", &resolve.executable); + verify_executable(&manifest.resource_type, "resolve", &resolve.executable, path.parent().unwrap()); capabilities.push(Capability::Resolve); } if let Some(SchemaKind::Command(command)) = &manifest.schema { - verify_executable(&manifest.resource_type, "schema", &command.executable); + verify_executable(&manifest.resource_type, "schema", &command.executable, path.parent().unwrap()); } let resource = DscResource { @@ -706,7 +707,7 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result< let mut capabilities: Vec = vec![]; if let Some(discover) = &manifest.discover { - verify_executable(&manifest.r#type, "discover", &discover.executable); + verify_executable(&manifest.r#type, "discover", &discover.executable, path.parent().unwrap()); capabilities.push(dscextension::Capability::Discover); } @@ -724,8 +725,8 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result< Ok(extension) } -fn verify_executable(resource: &str, operation: &str, executable: &str) { - if which(executable).is_err() { +fn verify_executable(resource: &str, operation: &str, executable: &str, directory: &Path) { + if canonicalize_which(executable, Some(directory.to_string_lossy().as_ref())).is_err() { warn!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = executable)); } } @@ -739,7 +740,7 @@ fn sort_adapters_based_on_lookup_table(unsorted_adapters: &BTreeMap Result { + SchemaKind::Command(command) => { let (_exit_code, stdout, _stderr) = invoke_command(&command.executable, command.args.clone(), None, Some(cwd), None, resource.exit_codes.as_ref())?; Ok(stdout) }, - SchemaKind::Embedded(ref schema) => { + SchemaKind::Embedded(schema) => { let json = serde_json::to_string(schema)?; Ok(json) }, @@ -662,7 +662,7 @@ async fn run_process_async(executable: &str, args: Option>, input: O if code != 0 { if let Some(exit_codes) = exit_codes { if let Some(error_message) = exit_codes.get(&code) { - return Err(DscError::CommandExitFromManifest(executable.to_string(), code, error_message.to_string())); + return Err(DscError::CommandExitFromManifest(executable.to_string(), code, error_message.clone())); } } return Err(DscError::Command(executable.to_string(), code, stderr_result)); @@ -697,12 +697,13 @@ async fn run_process_async(executable: &str, args: Option>, input: O #[allow(clippy::implicit_hasher)] pub fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { debug!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); + let executable = canonicalize_which(executable, cwd)?; tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() - .block_on(run_process_async(executable, args, input, cwd, env, exit_codes)) + .block_on(run_process_async(&executable, args, input, cwd, env, exit_codes)) } /// Process the arguments for a command resource. @@ -837,7 +838,7 @@ fn json_to_hashmap(json: &str) -> Result, DscError> { }, Value::Null => { // ignore null values - }, + }, Value::Object(_) => { return Err(DscError::Operation(t!("dscresources.commandResource.invalidKey", key = key).to_string())); }, diff --git a/dsc_lib/src/dscresources/dscresource.rs b/dsc_lib/src/dscresources/dscresource.rs index c4119c697..ee38ccf54 100644 --- a/dsc_lib/src/dscresources/dscresource.rs +++ b/dsc_lib/src/dscresources/dscresource.rs @@ -517,7 +517,7 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec { let sub_diff = get_diff(value, &actual[key]); if !sub_diff.is_empty() { debug!("{}", t!("dscresources.dscresource.subDiff", key = key)); - diff_properties.push(key.to_string()); + diff_properties.push(key.clone()); } } else { @@ -532,22 +532,22 @@ pub fn get_diff(expected: &Value, actual: &Value) -> Vec { if let Some(actual_array) = actual[key].as_array() { if !is_same_array(value_array, actual_array) { info!("{}", t!("dscresources.dscresource.diffArray", key = key)); - diff_properties.push(key.to_string()); + diff_properties.push(key.clone()); } } else { info!("{}", t!("dscresources.dscresource.diffNotArray", key = actual[key])); - diff_properties.push(key.to_string()); + diff_properties.push(key.clone()); } } else if value != &actual[key] { - diff_properties.push(key.to_string()); + diff_properties.push(key.clone()); } } else { info!("{}", t!("dscresources.dscresource.diffKeyMissing", key = key)); - diff_properties.push(key.to_string()); + diff_properties.push(key.clone()); } } else { info!("{}", t!("dscresources.dscresource.diffKeyNotObject", key = key)); - diff_properties.push(key.to_string()); + diff_properties.push(key.clone()); } } } diff --git a/dsc_lib/src/dscresources/invoke_result.rs b/dsc_lib/src/dscresources/invoke_result.rs index 9d7404a12..a48978770 100644 --- a/dsc_lib/src/dscresources/invoke_result.rs +++ b/dsc_lib/src/dscresources/invoke_result.rs @@ -93,10 +93,10 @@ pub enum TestResult { #[must_use] pub fn get_in_desired_state(test_result: &TestResult) -> bool { match test_result { - TestResult::Resource(ref resource_test_result) => { + TestResult::Resource(resource_test_result) => { resource_test_result.in_desired_state }, - TestResult::Group(ref group_test_result) => { + TestResult::Group(group_test_result) => { for result in group_test_result { if !get_in_desired_state(&(result.result)) { return false; diff --git a/dsc_lib/src/util.rs b/dsc_lib/src/util.rs index 454f02f75..a21776b50 100644 --- a/dsc_lib/src/util.rs +++ b/dsc_lib/src/util.rs @@ -4,14 +4,14 @@ use crate::dscerror::DscError; use rust_i18n::t; use serde_json::Value; -use std::fs; +use std::{fs, fs::canonicalize}; use std::fs::File; use std::io::BufReader; use std::path::PathBuf; use std::path::Path; use std::env; use tracing::debug; - +use which::which; pub struct DscSettingValue { pub setting: Value, pub policy: Value, @@ -176,3 +176,33 @@ fn get_settings_policy_file_path() -> String // This location is writable only by admins, but readable by all users Path::new("/etc").join("dsc").join("dsc.settings.json").display().to_string() } + +/// Returns the canonicalized path to the executable if it exists in PATH or in the specified current working directory. +/// +/// # Arguments +/// * `executable` - The name of the executable to find. +/// * `cwd` - An optional current working directory to search in if the executable is not found in PATH. +/// +/// # Returns +/// A string containing the canonicalized path to the executable. +/// +/// # Errors +/// This function will return an error if the executable is not found in either PATH or the specified current working directory. +pub fn canonicalize_which(executable: &str, cwd: Option<&str>) -> Result { + // Use PathBuf to handle path separators robustly + let mut executable_path = PathBuf::from(executable); + if cfg!(target_os = "windows") && executable_path.extension().is_none() { + executable_path.set_extension("exe"); + } + if which(executable).is_err() { + if let Some(cwd) = cwd { + let cwd_path = Path::new(cwd); + if let Ok(canonical_path) = canonicalize(cwd_path.join(&executable_path)) { + return Ok(canonical_path.to_string_lossy().to_string()); + } + return Err(DscError::CommandOperation(t!("util.executableNotFoundInWorkingDirectory", executable = &executable, cwd = cwd_path.to_string_lossy()).to_string(), executable_path.to_string_lossy().to_string())); + } + return Err(DscError::CommandOperation(t!("util.executableNotFound", executable = &executable).to_string(), executable.to_string())); + } + Ok(executable.to_string()) +} diff --git a/dscecho/src/echo.rs b/dscecho/src/echo.rs index 10cf9612f..8485adb8b 100644 --- a/dscecho/src/echo.rs +++ b/dscecho/src/echo.rs @@ -5,18 +5,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -pub struct SecureString { - #[serde(rename = "secureString")] - pub value: String, -} - -#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] -pub struct SecureObject { - #[serde(rename = "secureObject")] - pub value: Value, -} - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(untagged)] pub enum Output {