Skip to content

Commit a048494

Browse files
committed
Merge branch 'gh-57/main/add-cidr-functions' of https://github.com/Gijsreyn/operation-methods into gh-57/main/add-cidr-functions
2 parents 31b1d3f + c35fe67 commit a048494

File tree

10 files changed

+228
-25
lines changed

10 files changed

+228
-25
lines changed

docs/reference/schemas/resource/properties/purge.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ To add this property to a resource's instance schema, define the property with t
4343
snippet:
4444

4545
```json
46-
"_inDesiredState": {
46+
"_purge": {
4747
"$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/purge.json"
4848
}
4949
```

dsc/tests/dsc_discovery.tests.ps1

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,55 @@ Describe 'tests for resource discovery' {
245245
$env:DSC_RESOURCE_PATH = $oldPath
246246
}
247247
}
248+
249+
It 'Resource manifest using relative path to exe: <path>' -TestCases @(
250+
@{ path = '../dscecho'; success = $true }
251+
@{ path = '../foo/dscecho'; success = $false }
252+
) {
253+
param($path, $success)
254+
$manifest = @"
255+
{
256+
"`$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
257+
"type": "Microsoft.DSC.Debug/Echo",
258+
"version": "1.0.0",
259+
"description": "Echo resource for testing and debugging purposes",
260+
"get": {
261+
"executable": "$path",
262+
"args": [
263+
{
264+
"jsonInputArg": "--input",
265+
"mandatory": true
266+
}
267+
]
268+
},
269+
"schema": {
270+
"command": {
271+
"executable": "$path"
272+
}
273+
}
274+
}
275+
"@
276+
$dscEcho = Get-Command dscecho -ErrorAction Stop
277+
# copy to testdrive
278+
Copy-Item -Path "$($dscEcho.Source)" -Destination $testdrive
279+
# create manifest in subfolder
280+
$subfolder = Join-Path $testdrive 'subfolder'
281+
New-Item -Path $subfolder -ItemType Directory -Force | Out-Null
282+
Set-Content -Path (Join-Path $subfolder 'test.dsc.resource.json') -Value $manifest
283+
284+
try {
285+
$env:DSC_RESOURCE_PATH = $subfolder
286+
$out = dsc resource get -r 'Microsoft.DSC.Debug/Echo' -i '{"output":"RelativePathTest"}' 2> "$testdrive/error.txt" | ConvertFrom-Json
287+
if ($success) {
288+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path "$testdrive/error.txt")
289+
$out.actualState.output | Should -BeExactly 'RelativePathTest'
290+
} else {
291+
$LASTEXITCODE | Should -Be 2 -Because (Get-Content -Raw -Path "$testdrive/error.txt")
292+
(Get-Content -Raw -Path "$testdrive/error.txt") | Should -Match "ERROR.*?Executable '\.\./foo/dscecho(\.exe)?' not found"
293+
}
294+
}
295+
finally {
296+
$env:DSC_RESOURCE_PATH = $null
297+
}
298+
}
248299
}

dsc/tests/dsc_extension_discover.tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ Describe 'Discover extension tests' {
138138
$out = dsc -l warn resource list 2> $TestDrive/error.log | ConvertFrom-Json
139139
$LASTEXITCODE | Should -Be 0
140140
$out.Count | Should -BeGreaterThan 0
141-
(Get-Content -Path "$TestDrive/error.log" -Raw) | Should -BeLike "*WARN Extension 'Microsoft.Windows.Appx/Discover' failed to discover resources: Command: Operation program not found for executable 'powershell'*" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String)
141+
(Get-Content -Path "$TestDrive/error.log" -Raw) | Should -BeLike "*WARN Extension 'Microsoft.Windows.Appx/Discover' failed to discover resources: Command: Operation Executable 'powershell' not found*" -Because (Get-Content -Path "$TestDrive/error.log" -Raw | Out-String)
142142
} finally {
143143
$env:PATH = $oldPath
144144
}

dsc/tests/dsc_parameters.tests.ps1

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,4 +577,128 @@ Describe 'Parameters tests' {
577577
$LASTEXITCODE | Should -Be 4
578578
$testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters'
579579
}
580+
581+
It 'Default value violates <constraint> constraint' -TestCases @(
582+
@{ constraint = 'minLength'; type = 'string'; value = 'ab'; min = 3; max = 20; errorMatch = 'minimum length' }
583+
@{ constraint = 'maxLength'; type = 'string'; value = 'verylongusernamethatexceedslimit'; min = 3; max = 20; errorMatch = 'maximum length' }
584+
@{ constraint = 'minValue'; type = 'int'; value = 0; min = 1; max = 65535; errorMatch = 'minimum value' }
585+
@{ constraint = 'maxValue'; type = 'int'; value = 99999; min = 1; max = 65535; errorMatch = 'maximum value' }
586+
) {
587+
param($constraint, $type, $value, $min, $max, $errorMatch)
588+
589+
if ($type -eq 'string') {
590+
$value = "'$value'"
591+
}
592+
593+
$minConstraint = if ($type -eq 'string') { "minLength: $min" } else { "minValue: $min" }
594+
$maxConstraint = if ($type -eq 'string') { "maxLength: $max" } else { "maxValue: $max" }
595+
596+
$config_yaml = @"
597+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
598+
parameters:
599+
param1:
600+
type: $type
601+
$minConstraint
602+
$maxConstraint
603+
defaultValue: $value
604+
resources:
605+
- name: Echo
606+
type: Microsoft.DSC.Debug/Echo
607+
properties:
608+
output: '[parameters(''param1'')]'
609+
"@
610+
611+
$testError = & {$config_yaml | dsc config get -f - 2>&1}
612+
$LASTEXITCODE | Should -Be 4
613+
$testError | Should -Match $errorMatch
614+
}
615+
616+
It 'Default value violates allowedValues constraint for <type>' -TestCases @(
617+
@{ type = 'string'; value = 'staging'; allowed = @('dev', 'test', 'prod') }
618+
@{ type = 'int'; value = 7; allowed = @(1, 5, 10) }
619+
) {
620+
param($type, $value, $allowed)
621+
622+
if ($type -eq 'string') {
623+
$value = "'$value'"
624+
}
625+
626+
$config_yaml = @"
627+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
628+
parameters:
629+
param1:
630+
type: $type
631+
allowedValues: $($allowed | ConvertTo-Json -Compress)
632+
defaultValue: $value
633+
resources:
634+
- name: Echo
635+
type: Microsoft.DSC.Debug/Echo
636+
properties:
637+
output: '[parameters(''param1'')]'
638+
"@
639+
640+
$testError = & {$config_yaml | dsc config get -f - 2>&1}
641+
$LASTEXITCODE | Should -Be 4
642+
$testError | Should -Match 'allowed values'
643+
}
644+
645+
It 'Default values pass constraint validation for <type>' -TestCases @(
646+
@{ type = 'string'; value = 'admin'; min = 3; max = 20 }
647+
@{ type = 'string'; value = 'abc'; min = 3; max = 20 }
648+
@{ type = 'string'; value = 'abcdefghijklmnopqrst'; min = 3; max = 20 }
649+
@{ type = 'int'; value = 8080; min = 1; max = 65535 }
650+
@{ type = 'int'; value = 1; min = 1; max = 65535 }
651+
@{ type = 'int'; value = 65535; min = 1; max = 65535 }
652+
) {
653+
param($type, $value, $min, $max)
654+
655+
$minConstraint = if ($type -eq 'string') { "minLength: $min" } else { "minValue: $min" }
656+
$maxConstraint = if ($type -eq 'string') { "maxLength: $max" } else { "maxValue: $max" }
657+
658+
$config_yaml = @"
659+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
660+
parameters:
661+
param1:
662+
type: $type
663+
$minConstraint
664+
$maxConstraint
665+
defaultValue: $value
666+
resources:
667+
- name: Echo
668+
type: Microsoft.DSC.Debug/Echo
669+
properties:
670+
output: '[parameters(''param1'')]'
671+
"@
672+
673+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
674+
$LASTEXITCODE | Should -Be 0
675+
$out.results[0].result.actualState.output | Should -BeExactly $value
676+
}
677+
678+
It 'Default values with allowedValues pass validation for <type>' -TestCases @(
679+
@{ type = 'string'; value = 'dev'; allowed = @('dev', 'test', 'prod') }
680+
@{ type = 'string'; value = 'prod'; allowed = @('dev', 'test', 'prod') }
681+
@{ type = 'int'; value = 5; allowed = @(1, 5, 10) }
682+
@{ type = 'int'; value = 10; allowed = @(1, 5, 10) }
683+
) {
684+
param($type, $value, $allowed)
685+
686+
$config_yaml = @"
687+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
688+
parameters:
689+
param1:
690+
type: $type
691+
allowedValues: $($allowed | ConvertTo-Json -Compress)
692+
defaultValue: $value
693+
resources:
694+
- name: Echo
695+
type: Microsoft.DSC.Debug/Echo
696+
properties:
697+
output: '[parameters(''param1'')]'
698+
"@
699+
700+
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
701+
$LASTEXITCODE | Should -Be 0
702+
$out.results[0].result.actualState.output | Should -BeExactly $value
703+
}
580704
}

lib/dsc-lib/locales/en-us.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,3 +724,5 @@ failedToGetExePath = "Can't get 'dsc' executable path"
724724
settingNotFound = "Setting '%{name}' not found"
725725
failedToAbsolutizePath = "Failed to absolutize path '%{path}'"
726726
invalidExitCodeKey = "Invalid exit code key '%{key}'"
727+
executableNotFoundInWorkingDirectory = "Executable '%{executable}' not found with working directory '%{cwd}'"
728+
executableNotFound = "Executable '%{executable}' not found"

lib/dsc-lib/src/configure/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,9 @@ impl Configurator {
895895
};
896896

897897
if let Ok(value) = value_result {
898+
check_length(name, &value, parameter)?;
899+
check_allowed_values(name, &value, parameter)?;
900+
check_number_limits(name, &value, parameter)?;
898901
validate_parameter_type(name, &value, &parameter.parameter_type)?;
899902
self.context.parameters.insert(name.to_string(), (value, parameter.parameter_type.clone()));
900903
resolved_in_this_pass.push(name.clone());

lib/dsc-lib/src/discovery/command_discovery.rs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,13 @@ use serde::Deserialize;
2020
use std::{collections::{BTreeMap, HashMap, HashSet}, sync::{LazyLock, RwLock}};
2121
use std::env;
2222
use std::ffi::OsStr;
23-
use std::fs;
23+
use std::fs::{create_dir_all, read, read_to_string, write};
2424
use std::path::{Path, PathBuf};
2525
use std::str::FromStr;
2626
use tracing::{debug, info, trace, warn};
27-
use which::which;
2827

2928
use crate::util::get_setting;
30-
use crate::util::get_exe_path;
29+
use crate::util::{canonicalize_which, get_exe_path};
3130

3231
const DSC_EXTENSION_EXTENSIONS: [&str; 3] = [".dsc.extension.json", ".dsc.extension.yaml", ".dsc.extension.yml"];
3332
const DSC_MANIFEST_LIST_EXTENSIONS: [&str; 3] = [".dsc.manifests.json", ".dsc.manifests.yaml", ".dsc.manifests.yml"];
@@ -621,7 +620,7 @@ fn insert_resource(resources: &mut BTreeMap<String, Vec<DscResource>>, resource:
621620
///
622621
/// * Returns a `DscError` if the manifest could not be loaded or parsed.
623622
pub fn load_manifest(path: &Path) -> Result<Vec<ImportedManifest>, DscError> {
624-
let contents = fs::read_to_string(path)?;
623+
let contents = read_to_string(path)?;
625624
let file_name_lowercase = path.file_name().and_then(OsStr::to_str).unwrap_or("").to_lowercase();
626625
let extension_is_json = path.extension().is_some_and(|ext| ext.eq_ignore_ascii_case("json"));
627626
if DSC_RESOURCE_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) {
@@ -711,38 +710,38 @@ fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result<Ds
711710

712711
let mut capabilities: Vec<Capability> = vec![];
713712
if let Some(get) = &manifest.get {
714-
verify_executable(&manifest.resource_type, "get", &get.executable);
713+
verify_executable(&manifest.resource_type, "get", &get.executable, path.parent().unwrap());
715714
capabilities.push(Capability::Get);
716715
}
717716
if let Some(set) = &manifest.set {
718-
verify_executable(&manifest.resource_type, "set", &set.executable);
717+
verify_executable(&manifest.resource_type, "set", &set.executable, path.parent().unwrap());
719718
capabilities.push(Capability::Set);
720719
if set.handles_exist == Some(true) {
721720
capabilities.push(Capability::SetHandlesExist);
722721
}
723722
}
724723
if let Some(what_if) = &manifest.what_if {
725-
verify_executable(&manifest.resource_type, "what_if", &what_if.executable);
724+
verify_executable(&manifest.resource_type, "what_if", &what_if.executable, path.parent().unwrap());
726725
capabilities.push(Capability::WhatIf);
727726
}
728727
if let Some(test) = &manifest.test {
729-
verify_executable(&manifest.resource_type, "test", &test.executable);
728+
verify_executable(&manifest.resource_type, "test", &test.executable, path.parent().unwrap());
730729
capabilities.push(Capability::Test);
731730
}
732731
if let Some(delete) = &manifest.delete {
733-
verify_executable(&manifest.resource_type, "delete", &delete.executable);
732+
verify_executable(&manifest.resource_type, "delete", &delete.executable, path.parent().unwrap());
734733
capabilities.push(Capability::Delete);
735734
}
736735
if let Some(export) = &manifest.export {
737-
verify_executable(&manifest.resource_type, "export", &export.executable);
736+
verify_executable(&manifest.resource_type, "export", &export.executable, path.parent().unwrap());
738737
capabilities.push(Capability::Export);
739738
}
740739
if let Some(resolve) = &manifest.resolve {
741-
verify_executable(&manifest.resource_type, "resolve", &resolve.executable);
740+
verify_executable(&manifest.resource_type, "resolve", &resolve.executable, path.parent().unwrap());
742741
capabilities.push(Capability::Resolve);
743742
}
744743
if let Some(SchemaKind::Command(command)) = &manifest.schema {
745-
verify_executable(&manifest.resource_type, "schema", &command.executable);
744+
verify_executable(&manifest.resource_type, "schema", &command.executable, path.parent().unwrap());
746745
}
747746

748747
let resource = DscResource {
@@ -768,15 +767,15 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result<
768767

769768
let mut capabilities: Vec<dscextension::Capability> = vec![];
770769
if let Some(discover) = &manifest.discover {
771-
verify_executable(&manifest.r#type, "discover", &discover.executable);
770+
verify_executable(&manifest.r#type, "discover", &discover.executable, path.parent().unwrap());
772771
capabilities.push(dscextension::Capability::Discover);
773772
}
774773
if let Some(secret) = &manifest.secret {
775-
verify_executable(&manifest.r#type, "secret", &secret.executable);
774+
verify_executable(&manifest.r#type, "secret", &secret.executable, path.parent().unwrap());
776775
capabilities.push(dscextension::Capability::Secret);
777776
}
778777
let import_extensions = if let Some(import) = &manifest.import {
779-
verify_executable(&manifest.r#type, "import", &import.executable);
778+
verify_executable(&manifest.r#type, "import", &import.executable, path.parent().unwrap());
780779
capabilities.push(dscextension::Capability::Import);
781780
if import.file_extensions.is_empty() {
782781
warn!("{}", t!("discovery.commandDiscovery.importExtensionsEmpty", extension = manifest.r#type));
@@ -803,8 +802,8 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result<
803802
Ok(extension)
804803
}
805804

806-
fn verify_executable(resource: &str, operation: &str, executable: &str) {
807-
if which(executable).is_err() {
805+
fn verify_executable(resource: &str, operation: &str, executable: &str, directory: &Path) {
806+
if canonicalize_which(executable, Some(directory.to_string_lossy().as_ref())).is_err() {
808807
info!("{}", t!("discovery.commandDiscovery.executableNotFound", resource = resource, operation = operation, executable = executable));
809808
}
810809
}
@@ -839,8 +838,8 @@ fn save_adapted_resources_lookup_table(lookup_table: &HashMap<String, String>)
839838

840839
let path = std::path::Path::new(&file_path);
841840
if let Some(prefix) = path.parent() {
842-
if fs::create_dir_all(prefix).is_ok() {
843-
if fs::write(file_path.clone(), lookup_table_json).is_err() {
841+
if create_dir_all(prefix).is_ok() {
842+
if write(file_path.clone(), lookup_table_json).is_err() {
844843
info!("Unable to write lookup_table file {file_path:?}");
845844
}
846845
} else {
@@ -858,7 +857,7 @@ fn load_adapted_resources_lookup_table() -> HashMap<String, String>
858857
{
859858
let file_path = get_lookup_table_file_path();
860859

861-
let lookup_table: HashMap<String, String> = match fs::read(file_path.clone()){
860+
let lookup_table: HashMap<String, String> = match read(file_path.clone()){
862861
Ok(data) => { serde_json::from_slice(&data).unwrap_or_default() },
863862
Err(_) => { HashMap::new() }
864863
};

lib/dsc-lib/src/dscerror.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ pub enum DscError {
2626
#[error("{t} '{0}' [{t2} {1}] {t3}: {2}", t = t!("dscerror.commandResource"), t2 = t!("dscerror.exitCode"), t3 = t!("dscerror.manifestDescription"))]
2727
CommandExitFromManifest(String, i32, String),
2828

29+
#[error("{0}")]
30+
CommandNotFound(String),
31+
2932
#[error("{t} {0} {t2} '{1}'", t = t!("dscerror.commandOperation"), t2 = t!("dscerror.forExecutable"))]
3033
CommandOperation(String, String),
3134

lib/dsc-lib/src/dscresources/command_resource.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use rust_i18n::t;
77
use serde::Deserialize;
88
use serde_json::{Map, Value};
99
use std::{collections::HashMap, env, process::Stdio};
10-
use crate::configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}};
10+
use crate::{configure::{config_doc::ExecutionKind, config_result::{ResourceGetResult, ResourceTestResult}}, util::canonicalize_which};
1111
use crate::dscerror::DscError;
1212
use super::{dscresource::{get_diff, redact}, invoke_result::{ExportResult, GetResult, ResolveResult, SetResult, TestResult, ValidateResult, ResourceGetResponse, ResourceSetResponse, ResourceTestResponse, get_in_desired_state}, resource_manifest::{ArgKind, InputKind, Kind, ResourceManifest, ReturnKind, SchemaKind}};
1313
use tracing::{error, warn, info, debug, trace};
@@ -763,6 +763,7 @@ fn convert_hashmap_string_keys_to_i32(input: Option<&HashMap<String, String>>) -
763763
#[allow(clippy::implicit_hasher)]
764764
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<String, String>>) -> Result<(i32, String, String), DscError> {
765765
let exit_codes = convert_hashmap_string_keys_to_i32(exit_codes)?;
766+
let executable = canonicalize_which(executable, cwd)?;
766767

767768
tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on(
768769
async {
@@ -771,7 +772,7 @@ pub fn invoke_command(executable: &str, args: Option<Vec<String>>, input: Option
771772
trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd));
772773
}
773774

774-
match run_process_async(executable, args, input, cwd, env, exit_codes.as_ref()).await {
775+
match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await {
775776
Ok((code, stdout, stderr)) => {
776777
Ok((code, stdout, stderr))
777778
},

0 commit comments

Comments
 (0)