Skip to content

Commit ccdda45

Browse files
committed
Add tryWhich() function and enable manifests to have condition
1 parent f7e6e82 commit ccdda45

File tree

12 files changed

+275
-38
lines changed

12 files changed

+275
-38
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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+
) {
9+
param($condition, $shouldBeFound)
10+
11+
$extension_manifest = @"
12+
{
13+
"`$schema": "https://aka.ms/dsc/schemas/v3/bundled/extension/manifest.json",
14+
"type": "Test/Extension",
15+
"condition": "$condition",
16+
"version": "0.1.0",
17+
"import": {
18+
"fileExtensions": ["foo"],
19+
"executable": "dsc"
20+
}
21+
}
22+
"@
23+
24+
try {
25+
$env:DSC_RESOURCE_PATH = $TestDrive
26+
$manifestPath = Join-Path -Path $TestDrive -ChildPath 'Extension.dsc.extension.json'
27+
$extension_manifest | Out-File -FilePath $manifestPath -Encoding utf8
28+
$extensions = dsc extension list | ConvertFrom-Json -Depth 10
29+
$LASTEXITCODE | Should -Be 0
30+
if ($shouldBeFound) {
31+
$extensions.count | Should -Be 1
32+
$extensions.type | Should -BeExactly 'Test/Extension'
33+
}
34+
else {
35+
$extensions.count | Should -Be 0
36+
}
37+
} finally {
38+
$env:DSC_RESOURCE_PATH = $null
39+
}
40+
}
41+
}

dsc/tests/dsc_functions.tests.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,4 +1204,27 @@ Describe 'tests for function expressions' {
12041204
$expected = "Microsoft.DSC.Debug/Echo:$([Uri]::EscapeDataString($name))"
12051205
$out.results[0].result.actualState.output | Should -BeExactly $expected
12061206
}
1207+
1208+
It 'tryWhich() works for: <expression>' -TestCases @(
1209+
@{ expression = "[tryWhich('pwsh')]"; found = $true }
1210+
@{ expression = "[tryWhich('nonexistentcommand12345')]"; found = $false }
1211+
) {
1212+
param($expression, $found)
1213+
1214+
$config_yaml = @"
1215+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1216+
resources:
1217+
- name: Echo
1218+
type: Microsoft.DSC.Debug/Echo
1219+
properties:
1220+
output: "$expression"
1221+
"@
1222+
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
1223+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
1224+
if ($found) {
1225+
$out.results[0].result.actualState.output | Should -Not -BeNullOrEmpty
1226+
} else {
1227+
$out.results[0].result.actualState.output | Should -BeNullOrEmpty
1228+
}
1229+
}
12071230
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
) {
9+
param($condition, $shouldBeFound)
10+
11+
$resource_manifest = @"
12+
{
13+
"`$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
14+
"type": "Test/MyEcho",
15+
"version": "1.0.0",
16+
"condition": "$condition",
17+
"get": {
18+
"executable": "dscecho",
19+
"args": [
20+
{
21+
"jsonInputArg": "--input",
22+
"mandatory": true
23+
}
24+
]
25+
},
26+
"schema": {
27+
"command": {
28+
"executable": "dscecho"
29+
}
30+
}
31+
}
32+
"@
33+
34+
try {
35+
$env:DSC_RESOURCE_PATH = $TestDrive
36+
$manifestPath = Join-Path -Path $TestDrive -ChildPath 'MyEcho.dsc.resource.json'
37+
$resource_manifest | Out-File -FilePath $manifestPath -Encoding utf8
38+
$resources = dsc resource list | ConvertFrom-Json -Depth 10
39+
$LASTEXITCODE | Should -Be 0
40+
if ($shouldBeFound) {
41+
$resources.count | Should -Be 1
42+
$resources.type | Should -BeExactly 'Test/MyEcho'
43+
}
44+
else {
45+
$resources.count | Should -Be 0
46+
}
47+
} finally {
48+
$env:DSC_RESOURCE_PATH = $null
49+
}
50+
}
51+
}

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
@@ -111,6 +111,8 @@ foundResourceWithVersion = "Found matching resource '%{resource}' version %{vers
111111
foundNonAdapterResources = "Found %{count} non-adapter resources"
112112
resourceMissingRequireAdapter = "Resource '%{resource}' is missing 'require_adapter' field."
113113
extensionDiscoverFailed = "Extension '%{extension}' failed to discover resources: %{error}"
114+
conditionNotBoolean = "Condition '%{condition}' did not evaluate to a boolean"
115+
conditionNotMet = "Condition '%{condition}' not met, skipping manifest at '%{path}'"
114116

115117
[dscresources.commandResource]
116118
invokeGet = "Invoking get for '%{resource}'"
@@ -535,6 +537,10 @@ invoked = "tryGet function"
535537
invalidKeyType = "Invalid key type, must be a string"
536538
invalidIndexType = "Invalid index type, must be an integer"
537539

540+
[functions.tryWhich]
541+
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."
542+
invoked = "tryWhich function"
543+
538544
[functions.union]
539545
description = "Returns a single array or object with all elements from the parameters"
540546
invoked = "union function"

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

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

4-
use crate::discovery::{
5-
discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}
6-
};
4+
use crate::{discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind, ResourceDiscovery}, parser::Statement};
75
use crate::{locked_is_empty, locked_extend, locked_clone, locked_get};
6+
use crate::configure::context::Context;
87
use crate::dscresources::dscresource::{Capability, DscResource, ImplementedAs};
98
use crate::dscresources::resource_manifest::{import_manifest, validate_semver, Kind, ResourceManifest, SchemaKind};
109
use crate::dscresources::command_resource::invoke_command;
@@ -259,37 +258,39 @@ impl ResourceDiscovery for CommandDiscovery {
259258
},
260259
};
261260

262-
match resource {
263-
ImportedManifest::Extension(extension) => {
264-
if regex.is_match(&extension.type_name) {
265-
trace!("{}", t!("discovery.commandDiscovery.extensionFound", extension = extension.type_name, version = extension.version));
266-
// we only keep newest version of the extension so compare the version and only keep the newest
267-
if let Some(existing_extension) = extensions.get_mut(&extension.type_name) {
268-
let Ok(existing_version) = Version::parse(&existing_extension.version) else {
269-
return Err(DscError::Operation(t!("discovery.commandDiscovery.extensionInvalidVersion", extension = existing_extension.type_name, version = existing_extension.version).to_string()));
270-
};
271-
let Ok(new_version) = Version::parse(&extension.version) else {
272-
return Err(DscError::Operation(t!("discovery.commandDiscovery.extensionInvalidVersion", extension = extension.type_name, version = extension.version).to_string()));
273-
};
274-
if new_version > existing_version {
261+
if let Some(resource) = resource {
262+
match resource {
263+
ImportedManifest::Extension(extension) => {
264+
if regex.is_match(&extension.type_name) {
265+
trace!("{}", t!("discovery.commandDiscovery.extensionFound", extension = extension.type_name, version = extension.version));
266+
// we only keep newest version of the extension so compare the version and only keep the newest
267+
if let Some(existing_extension) = extensions.get_mut(&extension.type_name) {
268+
let Ok(existing_version) = Version::parse(&existing_extension.version) else {
269+
return Err(DscError::Operation(t!("discovery.commandDiscovery.extensionInvalidVersion", extension = existing_extension.type_name, version = existing_extension.version).to_string()));
270+
};
271+
let Ok(new_version) = Version::parse(&extension.version) else {
272+
return Err(DscError::Operation(t!("discovery.commandDiscovery.extensionInvalidVersion", extension = extension.type_name, version = extension.version).to_string()));
273+
};
274+
if new_version > existing_version {
275+
extensions.insert(extension.type_name.clone(), extension.clone());
276+
}
277+
} else {
275278
extensions.insert(extension.type_name.clone(), extension.clone());
276279
}
277-
} else {
278-
extensions.insert(extension.type_name.clone(), extension.clone());
279280
}
280-
}
281-
},
282-
ImportedManifest::Resource(resource) => {
283-
if regex.is_match(&resource.type_name) {
284-
if let Some(ref manifest) = resource.manifest {
285-
let manifest = import_manifest(manifest.clone())?;
286-
if manifest.kind == Some(Kind::Adapter) {
287-
trace!("{}", t!("discovery.commandDiscovery.adapterFound", adapter = resource.type_name, version = resource.version));
288-
insert_resource(&mut adapters, &resource);
281+
},
282+
ImportedManifest::Resource(resource) => {
283+
if regex.is_match(&resource.type_name) {
284+
if let Some(ref manifest) = resource.manifest {
285+
let manifest = import_manifest(manifest.clone())?;
286+
if manifest.kind == Some(Kind::Adapter) {
287+
trace!("{}", t!("discovery.commandDiscovery.adapterFound", adapter = resource.type_name, version = resource.version));
288+
insert_resource(&mut adapters, &resource);
289+
}
290+
// also make sure to add adapters as a resource as well
291+
trace!("{}", t!("discovery.commandDiscovery.resourceFound", resource = resource.type_name, version = resource.version));
292+
insert_resource(&mut resources, &resource);
289293
}
290-
// also make sure to add adapters as a resource as well
291-
trace!("{}", t!("discovery.commandDiscovery.resourceFound", resource = resource.type_name, version = resource.version));
292-
insert_resource(&mut resources, &resource);
293294
}
294295
}
295296
}
@@ -606,35 +607,63 @@ fn insert_resource(resources: &mut BTreeMap<String, Vec<DscResource>>, resource:
606607
/// # Errors
607608
///
608609
/// * Returns a `DscError` if the manifest could not be loaded or parsed.
609-
pub fn load_manifest(path: &Path) -> Result<ImportedManifest, DscError> {
610+
pub fn load_manifest(path: &Path) -> Result<Option<ImportedManifest>, DscError> {
610611
let contents = fs::read_to_string(path)?;
611612
if path.extension() == Some(OsStr::new("json")) {
612613
if let Ok(manifest) = serde_json::from_str::<ExtensionManifest>(&contents) {
614+
if !evaluate_condition(manifest.condition.as_deref())? {
615+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default()));
616+
return Ok(None);
617+
}
613618
let extension = load_extension_manifest(path, &manifest)?;
614-
return Ok(ImportedManifest::Extension(extension));
619+
return Ok(Some(ImportedManifest::Extension(extension)));
615620
}
616621
let manifest = match serde_json::from_str::<ResourceManifest>(&contents) {
617622
Ok(manifest) => manifest,
618623
Err(err) => {
619624
return Err(DscError::Manifest(t!("discovery.commandDiscovery.invalidManifest", resource = path.to_string_lossy()).to_string(), err));
620625
}
621626
};
627+
if !evaluate_condition(manifest.condition.as_deref())? {
628+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default()));
629+
return Ok(None);
630+
}
622631
let resource = load_resource_manifest(path, &manifest)?;
623-
return Ok(ImportedManifest::Resource(resource));
632+
return Ok(Some(ImportedManifest::Resource(resource)));
624633
}
625634

626635
if let Ok(manifest) = serde_yaml::from_str::<ResourceManifest>(&contents) {
636+
if !evaluate_condition(manifest.condition.as_deref())? {
637+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default()));
638+
return Ok(None);
639+
}
627640
let resource = load_resource_manifest(path, &manifest)?;
628-
return Ok(ImportedManifest::Resource(resource));
641+
return Ok(Some(ImportedManifest::Resource(resource)));
629642
}
630643
let manifest = match serde_yaml::from_str::<ExtensionManifest>(&contents) {
631644
Ok(manifest) => manifest,
632645
Err(err) => {
633646
return Err(DscError::Validation(t!("discovery.commandDiscovery.invalidManifest", path = path.to_string_lossy(), err = err).to_string()));
634647
}
635648
};
649+
if !evaluate_condition(manifest.condition.as_deref())? {
650+
debug!("{}", t!("discovery.commandDiscovery.conditionNotMet", path = path.to_string_lossy(), condition = manifest.condition.unwrap_or_default()));
651+
return Ok(None);
652+
}
636653
let extension = load_extension_manifest(path, &manifest)?;
637-
Ok(ImportedManifest::Extension(extension))
654+
Ok(Some(ImportedManifest::Extension(extension)))
655+
}
656+
657+
fn evaluate_condition(condition: Option<&str>) -> Result<bool, DscError> {
658+
if let Some(cond) = condition {
659+
let mut statement = Statement::new()?;
660+
let result = statement.parse_and_execute(cond, &Context::new())?;
661+
if let Some(bool_result) = result.as_bool() {
662+
return Ok(bool_result);
663+
}
664+
return Err(DscError::Validation(t!("discovery.commandDiscovery.conditionNotBoolean", condition = cond).to_string()));
665+
}
666+
Ok(true)
638667
}
639668

640669
fn load_resource_manifest(path: &Path, manifest: &ResourceManifest) -> Result<DscResource, DscError> {

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

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ impl DscExtension {
9090
}
9191
let manifest_path = Path::new(&discover_result.manifest_path);
9292
// Currently we don't support extensions discovering other extensions
93-
if let ImportedManifest::Resource(resource) = load_manifest(manifest_path)? {
93+
if let Some(ImportedManifest::Resource(resource)) = load_manifest(manifest_path)? {
9494
resources.push(resource);
9595
}
9696
}

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
}

0 commit comments

Comments
 (0)