Skip to content

Commit 1310751

Browse files
authored
Merge pull request #1194 from SteveL-MSFT/which-function
Add `tryWhich()` function and enable manifests to have `condition`
2 parents da8f514 + 984460d commit 1310751

File tree

12 files changed

+249
-8
lines changed

12 files changed

+249
-8
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Extension Manifests' {
5+
It 'Extension manifests with condition: <condition>' -TestCases @(
6+
@{ condition = "[equals(1, 1)]"; shouldBeFound = $true }
7+
@{ condition = "[equals(1, 0)]"; shouldBeFound = $false }
8+
@{ condition = "[equals(context().os.family,'macOS')]"; shouldBeFound = $IsMacOS }
9+
@{ condition = "[equals(context().os.family,'Linux')]"; shouldBeFound = $IsLinux }
10+
@{ condition = "[equals(context().os.family,'Windows')]"; shouldBeFound = $IsWindows }
11+
) {
12+
param($condition, $shouldBeFound)
13+
14+
$extension_manifest = @"
15+
{
16+
"`$schema": "https://aka.ms/dsc/schemas/v3/bundled/extension/manifest.json",
17+
"type": "Test/Extension",
18+
"condition": "$condition",
19+
"version": "0.1.0",
20+
"import": {
21+
"fileExtensions": ["foo"],
22+
"executable": "dsc"
23+
}
24+
}
25+
"@
26+
27+
try {
28+
$env:DSC_RESOURCE_PATH = $TestDrive
29+
$manifestPath = Join-Path -Path $TestDrive -ChildPath 'Extension.dsc.extension.json'
30+
$extension_manifest | Out-File -FilePath $manifestPath -Encoding utf8
31+
$extensions = dsc extension list | ConvertFrom-Json -Depth 10
32+
$LASTEXITCODE | Should -Be 0
33+
if ($shouldBeFound) {
34+
$extensions.count | Should -Be 1
35+
$extensions.type | Should -BeExactly 'Test/Extension'
36+
}
37+
else {
38+
$extensions.count | Should -Be 0
39+
}
40+
} finally {
41+
$env:DSC_RESOURCE_PATH = $null
42+
}
43+
}
44+
}

dsc/tests/dsc_functions.tests.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,4 +1594,27 @@ Describe 'tests for function expressions' {
15941594
$LASTEXITCODE | Should -Be 2
15951595
$errorContent | Should -Match $errorMatch
15961596
}
1597+
1598+
It 'tryWhich() works for: <expression>' -TestCases @(
1599+
@{ expression = "[tryWhich('pwsh')]"; found = $true }
1600+
@{ expression = "[tryWhich('nonexistentcommand12345')]"; found = $false }
1601+
) {
1602+
param($expression, $found)
1603+
1604+
$config_yaml = @"
1605+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1606+
resources:
1607+
- name: Echo
1608+
type: Microsoft.DSC.Debug/Echo
1609+
properties:
1610+
output: "$expression"
1611+
"@
1612+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
1613+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
1614+
if ($found) {
1615+
$out.results[0].result.actualState.output | Should -Not -BeNullOrEmpty
1616+
} else {
1617+
$out.results[0].result.actualState.output | Should -BeNullOrEmpty
1618+
}
1619+
}
15971620
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Resource Manifests' {
5+
It 'Resource manifests with condition: <condition>' -TestCases @(
6+
@{ condition = "[equals(1, 1)]"; shouldBeFound = $true }
7+
@{ condition = "[equals(1, 0)]"; shouldBeFound = $false }
8+
@{ condition = "[equals(context().os.family,'macOS')]"; shouldBeFound = $IsMacOS }
9+
@{ condition = "[equals(context().os.family,'Linux')]"; shouldBeFound = $IsLinux }
10+
@{ condition = "[equals(context().os.family,'Windows')]"; shouldBeFound = $IsWindows }
11+
) {
12+
param($condition, $shouldBeFound)
13+
14+
$resource_manifest = @"
15+
{
16+
"`$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
17+
"type": "Test/MyEcho",
18+
"version": "1.0.0",
19+
"condition": "$condition",
20+
"get": {
21+
"executable": "dscecho",
22+
"args": [
23+
{
24+
"jsonInputArg": "--input",
25+
"mandatory": true
26+
}
27+
]
28+
},
29+
"schema": {
30+
"command": {
31+
"executable": "dscecho"
32+
}
33+
}
34+
}
35+
"@
36+
37+
try {
38+
$env:DSC_RESOURCE_PATH = $TestDrive
39+
$manifestPath = Join-Path -Path $TestDrive -ChildPath 'MyEcho.dsc.resource.json'
40+
$resource_manifest | Out-File -FilePath $manifestPath -Encoding utf8
41+
$resources = dsc resource list | ConvertFrom-Json -Depth 10
42+
$LASTEXITCODE | Should -Be 0
43+
if ($shouldBeFound) {
44+
$resources.count | Should -Be 1
45+
$resources.type | Should -BeExactly 'Test/MyEcho'
46+
}
47+
else {
48+
$resources.count | Should -Be 0
49+
}
50+
} finally {
51+
$env:DSC_RESOURCE_PATH = $null
52+
}
53+
}
54+
}

extensions/bicep/bicep.dsc.extension.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"type": "Microsoft.DSC.Extension/Bicep",
44
"version": "0.1.0",
55
"description": "Enable passing Bicep file directly to DSC, but requires bicep executable to be available.",
6+
"condition": "[not(equals(tryWhich('bicep'), null()))]",
67
"import": {
78
"fileExtensions": ["bicep"],
89
"executable": "bicep",

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ foundResourceWithVersion = "Found matching resource '%{resource}' version %{vers
118118
foundNonAdapterResources = "Found %{count} non-adapter resources"
119119
resourceMissingRequireAdapter = "Resource '%{resource}' is missing 'require_adapter' field."
120120
extensionDiscoverFailed = "Extension '%{extension}' failed to discover resources: %{error}"
121+
conditionNotBoolean = "Condition '%{condition}' did not evaluate to a boolean"
122+
conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}'"
121123

122124
[dscresources.commandResource]
123125
invokeGet = "Invoking get for '%{resource}'"
@@ -588,6 +590,10 @@ invoked = "tryIndexFromEnd function"
588590
invalidSourceType = "Invalid source type, must be an array"
589591
invalidIndexType = "Invalid index type, must be an integer"
590592

593+
[functions.tryWhich]
594+
description = "Attempts to locate an executable in the system PATH. Null is returned if the executable is not found otherwise the full path to the executable is returned."
595+
invoked = "tryWhich function"
596+
591597
[functions.union]
592598
description = "Returns a single array or object with all elements from the parameters"
593599
invoked = "union function"

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

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::{discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}};
4+
use crate::{discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, parser::Statement};
55
use crate::{locked_is_empty, locked_extend, locked_clone, locked_get};
6+
use crate::configure::context::Context;
67
use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs};
78
use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest, SchemaKind};
89
use crate::dscresources::command_resource::invoke_command;
@@ -606,6 +607,18 @@ fn insert_resource(resources: &mut BTreeMap<String, Vec<DscResource>>, resource:
606607
}
607608
}
608609

610+
fn evaluate_condition(condition: Option<&str>) -> Result<bool, DscError> {
611+
if let Some(cond) = condition {
612+
let mut statement = Statement::new()?;
613+
let result = statement.parse_and_execute(cond, &Context::new())?;
614+
if let Some(bool_result) = result.as_bool() {
615+
return Ok(bool_result);
616+
}
617+
return Err(DscError::Validation(t!("discovery.commandDiscovery.conditionNotBoolean", condition = cond).to_string()));
618+
}
619+
Ok(true)
620+
}
621+
609622
/// Loads a manifest from the given path and returns a vector of `ImportedManifest`.
610623
///
611624
/// # Arguments
@@ -639,6 +652,10 @@ pub fn load_manifest(path: &Path) -> Result<Vec<ImportedManifest>, DscError> {
639652
}
640653
}
641654
};
655+
if !evaluate_condition(manifest.condition.as_deref())? {
656+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default()));
657+
return Ok(vec![]);
658+
}
642659
let resource = load_resource_manifest(path, &manifest)?;
643660
return Ok(vec![ImportedManifest::Resource(resource)]);
644661
}
@@ -658,10 +675,15 @@ pub fn load_manifest(path: &Path) -> Result<Vec<ImportedManifest>, DscError> {
658675
}
659676
}
660677
};
678+
if !evaluate_condition(manifest.condition.as_deref())? {
679+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default()));
680+
return Ok(vec![]);
681+
}
661682
let extension = load_extension_manifest(path, &manifest)?;
662683
return Ok(vec![ImportedManifest::Extension(extension)]);
663684
}
664685
if DSC_MANIFEST_LIST_EXTENSIONS.iter().any(|ext| file_name_lowercase.ends_with(ext)) {
686+
let mut resources: Vec<ImportedManifest> = vec![];
665687
let manifest_list = if extension_is_json {
666688
match serde_json::from_str::<ManifestList>(&contents) {
667689
Ok(manifest) => manifest,
@@ -677,15 +699,22 @@ pub fn load_manifest(path: &Path) -> Result<Vec<ImportedManifest>, DscError> {
677699
}
678700
}
679701
};
680-
let mut resources = vec![];
681702
if let Some(resource_manifests) = &manifest_list.resources {
682703
for res_manifest in resource_manifests {
704+
if !evaluate_condition(res_manifest.condition.as_deref())? {
705+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = res_manifest.condition.as_ref() : {:?}));
706+
continue;
707+
}
683708
let resource = load_resource_manifest(path, res_manifest)?;
684709
resources.push(ImportedManifest::Resource(resource));
685710
}
686711
}
687712
if let Some(extension_manifests) = &manifest_list.extensions {
688713
for ext_manifest in extension_manifests {
714+
if !evaluate_condition(ext_manifest.condition.as_deref())? {
715+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = ext_manifest.condition.as_ref() : {:?}));
716+
continue;
717+
}
689718
let extension = load_extension_manifest(path, ext_manifest)?;
690719
resources.push(ImportedManifest::Extension(extension));
691720
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ pub struct ResourceManifest {
3232
/// The namespaced name of the resource.
3333
#[serde(rename = "type")]
3434
pub resource_type: String,
35+
/// An optional condition for the resource to be active. If the condition evaluates to false, the resource is skipped.
36+
#[serde(skip_serializing_if = "Option::is_none")]
37+
pub condition: Option<String>,
3538
/// The kind of resource.
3639
#[serde(skip_serializing_if = "Option::is_none")]
3740
pub kind: Option<Kind>,

lib/dsc-lib/src/extensions/extension_manifest.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ pub struct ExtensionManifest {
2323
pub r#type: String,
2424
/// The version of the extension using semantic versioning.
2525
pub version: String,
26+
/// An optional condition for the extension to be active. If the condition evaluates to false, the extension is skipped.
27+
#[serde(skip_serializing_if = "Option::is_none")]
28+
pub condition: Option<String>,
2629
/// The description of the extension.
2730
pub description: Option<String>,
2831
/// Tags for the extension.

lib/dsc-lib/src/functions/equals.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ impl Function for Equals {
2020
min_args: 2,
2121
max_args: 2,
2222
accepted_arg_ordered_types: vec![
23-
vec![FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object],
24-
vec![FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object],
23+
vec![FunctionArgKind::Null, FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object],
24+
vec![FunctionArgKind::Null, FunctionArgKind::Number, FunctionArgKind::String, FunctionArgKind::Array, FunctionArgKind::Object],
2525
],
2626
remaining_arg_accepted_types: None,
2727
return_types: vec![FunctionArgKind::Boolean],
@@ -74,6 +74,13 @@ mod tests {
7474
assert_eq!(result, Value::Bool(false));
7575
}
7676

77+
#[test]
78+
fn null_equal() {
79+
let mut parser = Statement::new().unwrap();
80+
let result = parser.parse_and_execute("[equals(null(),null())]", &Context::new()).unwrap();
81+
assert_eq!(result, Value::Bool(true));
82+
}
83+
7784
// TODO: Add tests for arrays once `createArray()` is implemented
7885
// TODO: Add tests for objects once `createObject()` is implemented
7986
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ pub mod uri_component_to_string;
8585
pub mod user_function;
8686
pub mod utc_now;
8787
pub mod variables;
88+
pub mod try_which;
8889

8990
/// The kind of argument that a function accepts.
9091
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Serialize, JsonSchema)]
@@ -219,6 +220,7 @@ impl FunctionDispatcher {
219220
Box::new(uri_component_to_string::UriComponentToString{}),
220221
Box::new(utc_now::UtcNow{}),
221222
Box::new(variables::Variables{}),
223+
Box::new(try_which::TryWhich{}),
222224
];
223225
for function in function_list {
224226
functions.insert(function.get_metadata().name.clone(), function);

0 commit comments

Comments
 (0)