Skip to content

Commit

Permalink
Merge pull request #252 from SteveL-MSFT/resourceid-func
Browse files Browse the repository at this point in the history
Integrate expressions/functions with DSC
  • Loading branch information
SteveL-MSFT authored Nov 7, 2023
2 parents 8ba2f6a + 54a3880 commit 0ab0fdd
Show file tree
Hide file tree
Showing 16 changed files with 430 additions and 84 deletions.
6 changes: 6 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
],
"cwd": "${workspaceFolder}"
},
{
"name": "(macOS) Attach",
"type": "lldb",
"request": "attach",
"pid": "${command:pickMyProcess}",
},
{
"name": "(Windows) Attach",
"type": "cppvsdbg",
Expand Down
6 changes: 3 additions & 3 deletions dsc/examples/osinfo_registry.dsc.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@
"name": "windows product name",
"type": "Microsoft.Windows/Registry",
"properties": {
"keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion",
"valueName": "ProductName"
"keyPath": "[concat('HKLM\\','Software\\Microsoft\\Windows NT\\','CurrentVersion')]",
"valueName": "ProductName"
}
},
{
"name": "system root",
"type": "Microsoft.Windows/Registry",
"properties": {
"keyPath": "HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion",
"valueName": "SystemRoot"
"valueName": "SystemRoot"
}
}
]
Expand Down
4 changes: 2 additions & 2 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ fn terminate_subprocesses(sys: &System, process: &Process) {
#[cfg(debug_assertions)]
fn check_debug() {
if env::var("DEBUG_DSC").is_ok() {
error!("attach debugger to pid {} and press a key to continue", std::process::id());
eprintln!("attach debugger to pid {} and press a key to continue", std::process::id());
loop {
let event = event::read().unwrap();
if let event::Event::Key(key) = event {
Expand All @@ -147,7 +147,7 @@ fn check_debug() {
break;
}
} else {
error!("Unexpected event: {event:?}");
eprintln!("Unexpected event: {event:?}");
continue;
}
}
Expand Down
27 changes: 27 additions & 0 deletions dsc/tests/dsc_functions.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'tests for function expressions' {
It 'function works: <text>' -TestCases @(
@{ text = "[concat('a', 'b')]"; expected = 'ab' }
@{ text = "[concat('a', 'b', 'c')]"; expected = 'abc' }
@{ text = "[concat('a', 1, concat(2, 'b'))]"; expected = 'a12b' }
@{ text = "[base64('ab')]"; expected = 'YWI=' }
@{ text = "[base64(concat('a','b'))]"; expected = 'YWI=' }
@{ text = "[base64(base64(concat('a','b')))]"; expected = 'WVdJPQ==' }
) {
param($text, $expected)

$escapedText = $text -replace "'", "''"
$config_yaml = @"
`$schema: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/2023/10/config/document.json
resources:
- name: Echo
type: Test/Echo
properties:
text: '$escapedText'
"@
$out = $config_yaml | dsc config get | ConvertFrom-Json
$out.results[0].result.actualState.text | Should -Be $expected
}
}
6 changes: 2 additions & 4 deletions dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_json::{Map, Value};
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down Expand Up @@ -66,16 +66,14 @@ pub struct Resource {
/// The fully qualified name of the resource type
#[serde(rename = "type")]
pub resource_type: String,
// TODO: `apiVersion` is required by ARM but doesn't make sense here

/// A friendly name for the resource instance
pub name: String, // friendly unique instance name
#[serde(rename = "dependsOn", skip_serializing_if = "Option::is_none")]
#[schemars(regex(pattern = r"^\[resourceId\(\s*'[a-zA-Z0-9\.]+/[a-zA-Z0-9]+'\s*,\s*'[a-zA-Z0-9 ]+'\s*\)]$"))]
pub depends_on: Option<Vec<String>>,
// `identity` can be used for run-as
#[serde(skip_serializing_if = "Option::is_none")]
pub properties: Option<HashMap<String, Value>>,
pub properties: Option<Map<String, Value>>,
}

// Defines the valid and recognized canonical URIs for the configuration schema
Expand Down
76 changes: 43 additions & 33 deletions dsc_lib/src/configure/depends_on.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use regex::Regex;

use crate::configure::config_doc::Resource;
use crate::configure::Configuration;
use crate::DscError;
use crate::parser::Statement;

/// Gets the invocation order of resources based on their dependencies
///
///
/// # Arguments
///
///
/// * `config` - The configuration to get the invocation order for
///
///
/// # Returns
///
///
/// * `Result<Vec<Resource>, DscError>` - The invocation order of resources
///
///
/// # Errors
///
///
/// * `DscError::Validation` - The configuration is invalid
pub fn get_resource_invocation_order(config: &Configuration) -> Result<Vec<Resource>, DscError> {
pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statement) -> Result<Vec<Resource>, DscError> {
let mut order: Vec<Resource> = Vec::new();
let depends_on_regex = Regex::new(r"^\[resourceId\(\s*'(?<type>[a-zA-Z0-9\.]+/[a-zA-Z0-9]+)'\s*,\s*'(?<name>[a-zA-Z0-9 ]+)'\s*\)]$")?;
for resource in &config.resources {
// validate that the resource isn't specified more than once in the config
if config.resources.iter().filter(|r| r.name == resource.name && r.resource_type == resource.resource_type).count() > 1 {
Expand All @@ -32,12 +30,9 @@ pub fn get_resource_invocation_order(config: &Configuration) -> Result<Vec<Resou
let mut dependency_already_in_order = true;
if let Some(depends_on) = resource.depends_on.clone() {
for dependency in depends_on {
// validate dependency exists
let Some(captures) = depends_on_regex.captures(&dependency) else {
return Err(DscError::Validation(format!("'dependsOn' syntax is incorrect for resource name '{0}': {dependency}", resource.name)));
};
let resource_type = captures.name("type").ok_or(DscError::Validation("Resource type missing".to_string()))?.as_str();
let resource_name = captures.name("name").ok_or(DscError::Validation("Resource name missing".to_string()))?.as_str();
let statement = parser.parse_and_execute(&dependency)?;
let (resource_type, resource_name) = get_type_and_name(&statement)?;

// find the resource by name
let Some(dependency_resource) = config.resources.iter().find(|r| r.name.eq(resource_name)) else {
return Err(DscError::Validation(format!("'dependsOn' resource name '{resource_name}' does not exist for resource named '{0}'", resource.name)));
Expand Down Expand Up @@ -66,15 +61,12 @@ pub fn get_resource_invocation_order(config: &Configuration) -> Result<Vec<Resou
// check if the order has resource before its dependencies
let resource_index = order.iter().position(|r| r.name == resource.name && r.resource_type == resource.resource_type).ok_or(DscError::Validation("Resource not found in order".to_string()))?;
for dependency in depends_on {
let Some(captures) = depends_on_regex.captures(dependency) else {
return Err(DscError::Validation(format!("'dependsOn' syntax is incorrect for resource name '{0}': {dependency}", resource.name)));
};
let resource_type = captures.name("type").ok_or(DscError::Validation("Resource type not found in dependency".to_string()))?.as_str();
let resource_name = captures.name("name").ok_or(DscError::Validation("Resource name not found in dependency".to_string()))?.as_str();
let dependency_index = order.iter().position(|r| r.name == resource_name && r.resource_type == resource_type).ok_or(DscError::Validation("Dependency not found in order".to_string()))?;
if resource_index < dependency_index {
return Err(DscError::Validation(format!("Circular dependency detected for resource named '{0}'", resource.name)));
}
let statement = parser.parse_and_execute(dependency)?;
let (resource_type, resource_name) = get_type_and_name(&statement)?;
let dependency_index = order.iter().position(|r| r.name == resource_name && r.resource_type == resource_type).ok_or(DscError::Validation("Dependency not found in order".to_string()))?;
if resource_index < dependency_index {
return Err(DscError::Validation(format!("Circular dependency detected for resource named '{0}'", resource.name)));
}
}
}

Expand All @@ -87,8 +79,18 @@ pub fn get_resource_invocation_order(config: &Configuration) -> Result<Vec<Resou
Ok(order)
}

fn get_type_and_name(statement: &str) -> Result<(&str, &str), DscError> {
let parts: Vec<&str> = statement.split(':').collect();
if parts.len() != 2 {
return Err(DscError::Validation(format!("'dependsOn' syntax is incorrect: {statement}")));
}
Ok((parts[0], parts[1]))
}

#[cfg(test)]
mod tests {
use crate::parser;

use super::*;

#[test]
Expand All @@ -105,7 +107,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config).unwrap();
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser).unwrap();
assert_eq!(order[0].name, "First");
assert_eq!(order[1].name, "Second");
}
Expand All @@ -126,7 +129,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config);
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser);
assert!(order.is_err());
}

Expand All @@ -142,7 +146,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config);
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser);
assert!(order.is_err());
}

Expand All @@ -164,7 +169,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config).unwrap();
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser).unwrap();
assert_eq!(order[0].name, "First");
assert_eq!(order[1].name, "Second");
assert_eq!(order[2].name, "Third");
Expand All @@ -186,7 +192,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config);
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser);
assert!(order.is_err());
}

Expand All @@ -207,7 +214,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config).unwrap();
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser).unwrap();
assert_eq!(order[0].name, "First");
assert_eq!(order[1].name, "Second");
assert_eq!(order[2].name, "Third");
Expand All @@ -234,7 +242,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config);
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser);
assert!(order.is_err());
}

Expand All @@ -261,7 +270,8 @@ mod tests {
"#;

let config: Configuration = serde_yaml::from_str(config_yaml).unwrap();
let order = get_resource_invocation_order(&config).unwrap();
let mut parser = parser::Statement::new().unwrap();
let order = get_resource_invocation_order(&config, &mut parser).unwrap();
assert_eq!(order[0].name, "First");
assert_eq!(order[1].name, "Second");
assert_eq!(order[2].name, "Third");
Expand Down
Loading

0 comments on commit 0ab0fdd

Please sign in to comment.