Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions dsc/tests/dsc_functions.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Describe 'tests for function expressions' {
$out = dsc -l trace config get -i $config_yaml 2>$TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $TestDrive/error.log -Raw)
$out | Should -BeNullOrEmpty -Because "Output should be null or empty"
(Get-Content $TestDrive/error.log -Raw) | Should -Match 'utcNow function can only be used as a parameter default'
(Get-Content $TestDrive/error.log -Raw) | Should -Match "The 'utcNow\(\)' function can only be used as a parameter default"
}

It 'uniqueString function works for: <expression>' -TestCases @(
Expand Down Expand Up @@ -428,7 +428,7 @@ Describe 'tests for function expressions' {
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
($out.results[0].result.actualState.output | Out-String) | Should -BeExactly ($expected | Out-String)
}

It 'skip function works for: <expression>' -TestCases @(
@{ expression = "[skip(createArray('a','b','c','d'), 2)]"; expected = @('c', 'd') }
@{ expression = "[skip('hello', 2)]"; expected = 'llo' }
Expand Down
154 changes: 154 additions & 0 deletions dsc/tests/dsc_user_functions.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'user function tests' {
It 'user function working with expression: <expression>' -TestCases @(
@{ expression = "[MyFunction.ComboFunction('test', 42, true)]"; expected = 'test-42-True' }
@{ expression = "[MyOtherNamespace.ArrayFunction(createArray('a','b','c','d'))]"; expected = @('["b","c","d"]-a') }
) {
param($expression, $expected)

$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
functions:
- namespace: MyFunction
members:
ComboFunction:
parameters:
- name: StringParam
type: string
- name: NumberParam
type: int
- name: BoolParam
type: bool
output:
type: string
value: "[format('{0}-{1}-{2}', parameters('StringParam'), parameters('NumberParam'), parameters('BoolParam'))]"
- namespace: MyOtherNamespace
members:
ArrayFunction:
parameters:
- name: ArrayParam
type: array
output:
type: array
value: "[array(format('{0}-{1}', string(skip(parameters('ArrayParam'),1)), first(parameters('ArrayParam'))))]"
resources:
- name: test
type: Microsoft.DSC.Debug/Echo
properties:
output: "$expression"
"@

$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $testdrive/error.log | Out-String)
$out.results[0].result.actualState.output | Should -Be $expected -Because ($out | ConvertTo-Json -Depth 10 | Out-String)
}

It 'user function returning object works' {
$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
functions:
- namespace: MyObjectFunction
members:
ObjectFunction:
parameters:
- name: ObjectParam
type: object
output:
type: object
value: "[createObject('myKey', concat('#', string(parameters('ObjectParam'))))]"
resources:
- name: test
type: Microsoft.DSC.Debug/Echo
properties:
output: "[MyObjectFunction.ObjectFunction(createObject('key','value'))]"
"@

$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $testdrive/error.log | Out-String)
$out.results[0].result.actualState.output.myKey | Should -Be '#{"key":"value"}' -Because ($out | ConvertTo-Json -Depth 10 | Out-String)
}

It 'user functions cannot call function with expression: <expression>' -TestCases @(
@{ expression = "[reference('foo/bar')]"; errorText = "The 'reference()' function is not available in user-defined functions" }
@{ expression = "[utcNow()]"; errorText = "The 'utcNow()' function can only be used as a parameter default" }
@{ expression = "[variables('myVar')]"; errorText = "The 'variables()' function is not available in user-defined functions" }
@{ expression = "[MyFunction.OtherFunction()]"; errorText = "Unknown user function 'MyFunction.OtherFunction'" }
@{ expression = "[MyFunction.BadFunction()]"; errorText = "Unknown user function 'MyFunction.BadFunction'" }
) {
param($expression, $errorText)

$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
variables:
myVar: someValue
functions:
- namespace: MyFunction
members:
BadFunction:
output:
type: string
value: "$expression"
OtherFunction:
output:
type: string
value: "test"
resources:
- name: test
type: Microsoft.DSC.Debug/Echo
properties:
output: "[MyFunction.BadFunction()]"
"@

dsc -l trace config get -i $configYaml 2>$testdrive/error.log | Out-Null
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $testdrive/error.log | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -BeLike "*$errorText*" -Because (Get-Content $testdrive/error.log | Out-String)
}

It 'user function with invalid parameter fails' {
$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
functions:
- namespace: MyFunction
members:
BadFunction:
parameters:
- name: Param1
type: string
output:
type: string
value: "[parameters('BadParam')]"
resources:
- name: test
type: Microsoft.DSC.Debug/Echo
properties:
output: "[MyFunction.BadFunction('test')]"
"@

dsc -l trace config get -i $configYaml 2>$testdrive/error.log | Out-Null
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $testdrive/error.log | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -BeLike "*Parameter 'BadParam' not found in context*" -Because (Get-Content $testdrive/error.log | Out-String)
}

It 'user function with wrong output type fails' {
$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
functions:
- namespace: MyFunction
members:
BadFunction:
output:
type: int
value: "'this is a string'"
resources:
- name: test
type: Microsoft.DSC.Debug/Echo
properties:
output: "[MyFunction.BadFunction()]"
"@
dsc -l trace config get -i $configYaml 2>$testdrive/error.log | Out-Null
$LASTEXITCODE | Should -Be 2 -Because (Get-Content $testdrive/error.log | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -BeLike "*Output of user function 'MyFunction.BadFunction' did not return expected type 'int'*" -Because (Get-Content $testdrive/error.log | Out-String)
}
}
12 changes: 11 additions & 1 deletion dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ unrollingCopy = "Unrolling copy for resource '%{name}' with count %{count}"
copyModeNotSupported = "Copy mode is not supported"
copyBatchSizeNotSupported = "Copy batch size is not supported"
copyNameResultNotString = "Copy name result is not a string"
userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined"
addingUserFunction = "Adding user function '%{name}'"

[discovery.commandDiscovery]
couldNotReadSetting = "Could not read 'resourcePath' setting"
Expand Down Expand Up @@ -426,6 +428,7 @@ description = "Retrieves the output of a previously executed resource"
invoked = "reference function"
keyNotFound = "Invalid resourceId or resource has not executed yet: %{key}"
cannotUseInCopyMode = "The 'reference()' function cannot be used when processing a 'Copy' loop"
unavailableInUserFunction = "The 'reference()' function is not available in user-defined functions"

[functions.resourceId]
description = "Constructs a resource ID from the given type and name"
Expand Down Expand Up @@ -476,15 +479,22 @@ invalidArgType = "All arguments must either be arrays or objects"
description = "Returns a deterministic unique string from the given strings"
invoked = "uniqueString function"

[functions.userFunction]
expectedNoParameters = "User function '%{name}' does not accept parameters"
unknownUserFunction = "Unknown user function '%{name}'"
wrongParamCount = "User function '%{name}' expects %{expected} parameters, but %{got} were provided"
incorrectOutputType = "Output of user function '%{name}' did not return expected type '%{expected_type}'"

[functions.utcNow]
description = "Returns the current UTC time"
invoked = "utcNow function"
onlyUsedAsParameterDefault = "utcNow function can only be used as a parameter default"
onlyUsedAsParameterDefault = "The 'utcNow()' function can only be used as a parameter default"

[functions.variables]
description = "Retrieves the value of a variable"
invoked = "variables function"
keyNotFound = "Variable '%{key}' does not exist or has not been initialized yet"
unavailableInUserFunction = "The 'variables()' function is not available in user-defined functions"

[parser.expression]
functionNodeNotFound = "Function node not found"
Expand Down
48 changes: 45 additions & 3 deletions dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use rust_i18n::t;
use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::{collections::HashMap, fmt::Display};

use crate::{dscerror::DscError, schemas::DscRepoSchema};

Expand Down Expand Up @@ -105,6 +105,30 @@ pub struct Metadata {
pub other: Map<String, Value>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct UserFunction {
pub namespace: String,
pub members: HashMap<String, UserFunctionDefinition>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct UserFunctionDefinition {
pub parameters: Option<Vec<UserFunctionParameter>>,
pub output: UserFunctionOutput,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct UserFunctionParameter {
pub name: String,
pub r#type: DataType,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct UserFunctionOutput {
pub r#type: DataType,
pub value: String,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Configuration {
Expand All @@ -114,6 +138,8 @@ pub struct Configuration {
#[serde(rename = "contentVersion")]
pub content_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub functions: Option<Vec<UserFunction>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<HashMap<String, Parameter>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variables: Option<Map<String, Value>>,
Expand Down Expand Up @@ -162,6 +188,21 @@ pub enum DataType {
Array,
}

impl Display for DataType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let type_str = match self {
DataType::String => "string",
DataType::SecureString => "secureString",
DataType::Int => "int",
DataType::Bool => "bool",
DataType::Object => "object",
DataType::SecureObject => "secureObject",
DataType::Array => "array",
};
write!(f, "{type_str}")
}
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub enum CopyMode {
#[serde(rename = "serial")]
Expand Down Expand Up @@ -296,10 +337,11 @@ impl Configuration {
Self {
schema: Self::default_schema_id_uri(),
content_version: Some("1.0.0".to_string()),
metadata: None,
parameters: None,
variables: None,
resources: Vec::new(),
metadata: None,
functions: None,
variables: None,
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion dsc_lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// Licensed under the MIT License.

use chrono::{DateTime, Local};
use crate::{configure::config_doc::ExecutionKind, extensions::dscextension::DscExtension};
use crate::{configure::config_doc::{ExecutionKind, UserFunctionDefinition}, extensions::dscextension::DscExtension};
use security_context_lib::{get_security_context, SecurityContext};
use serde_json::{Map, Value};
use std::{collections::HashMap, path::PathBuf};
Expand Down Expand Up @@ -34,6 +34,7 @@ pub struct Context {
pub security_context: SecurityContextKind,
pub start_datetime: DateTime<Local>,
pub system_root: PathBuf,
pub user_functions: HashMap<String, UserFunctionDefinition>,
pub variables: Map<String, Value>,
}

Expand All @@ -58,6 +59,7 @@ impl Context {
},
start_datetime: chrono::Local::now(),
system_root: get_default_os_system_root(),
user_functions: HashMap::new(),
variables: Map::new(),
}
}
Expand Down
Loading
Loading