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
185 changes: 185 additions & 0 deletions dsc/tests/dsc_copy.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Tests for copy loops' {
It 'Works for resources' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex())]"
copy:
name: testLoop
count: 3
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@
$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.Count | Should -Be 3
$out.results[0].name | Should -Be 'Test-0'
$out.results[0].result.actualState.output | Should -Be 'Hello'
$out.results[1].name | Should -Be 'Test-1'
$out.results[1].result.actualState.output | Should -Be 'Hello'
$out.results[2].name | Should -Be 'Test-2'
$out.results[2].result.actualState.output | Should -Be 'Hello'
}

It 'copyIndex() works with offset' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex(10))]"
copy:
name: testLoop
count: 3
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@

$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.Count | Should -Be 3
$out.results[0].name | Should -Be 'Test-10'
$out.results[0].result.actualState.output | Should -Be 'Hello'
$out.results[1].name | Should -Be 'Test-11'
$out.results[1].result.actualState.output | Should -Be 'Hello'
$out.results[2].name | Should -Be 'Test-12'
$out.results[2].result.actualState.output | Should -Be 'Hello'
}

It 'copyIndex() with negative index returns error' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex(-1))]"
copy:
name: testLoop
count: 3
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@

$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -Match 'The offset cannot be negative'
}

It 'Copy works with count 0' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex())]"
copy:
name: testLoop
count: 0
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@

$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.Count | Should -Be 0
}

It 'copyIndex() with loop name works' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex('testLoop'))]"
copy:
name: testLoop
count: 3
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@
$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.Count | Should -Be 3
$out.results[0].name | Should -Be 'Test-0'
$out.results[0].result.actualState.output | Should -Be 'Hello'
$out.results[1].name | Should -Be 'Test-1'
$out.results[1].result.actualState.output | Should -Be 'Hello'
$out.results[2].name | Should -Be 'Test-2'
$out.results[2].result.actualState.output | Should -Be 'Hello'
}

It 'copyIndex() with invalid loop name "<name>" returns error' -TestCases @(
@{ name = "'noSuchLoop'" }
@{ name = "'noSuchLoop', 1" }
){
param($name)
$configYaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex($name))]"
copy:
name: testLoop
count: 3
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
"@

$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -Match "The specified loop name 'noSuchLoop' was not found"
}

It 'Copy mode is not supported' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex())]"
copy:
name: testLoop
count: 3
mode: serial
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -Match "Copy mode is not supported"
}

It 'Copy batch size is not supported' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[format('Test-{0}', copyIndex())]"
copy:
name: testLoop
count: 3
batchSize: 2
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -Match "Copy batch size is not supported"
}

It 'Name expression during copy must be a string' {
$configYaml = @'
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: "[copyIndex()]"
copy:
name: testLoop
count: 3
type: Microsoft.DSC.Debug/Echo
properties:
output: Hello
'@
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
(Get-Content $testdrive/error.log -Raw) | Should -Match "Copy name result is not a string"
}
}
13 changes: 13 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ metadataMicrosoftDscIgnored = "Resource returned '_metadata' property 'Microsoft
metadataNotObject = "Resource returned '_metadata' property which is not an object"
metadataRestartRequiredInvalid = "Resource returned '_metadata' property '_restartRequired' which contains invalid value: %{value}"
schemaExcludesMetadata = "Will not add '_metadata' to properties because resource schema does not support it"
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"

[discovery.commandDiscovery]
couldNotReadSetting = "Could not read 'resourcePath' setting"
Expand Down Expand Up @@ -259,6 +263,14 @@ invoked = "contains function"
invalidItemToFind = "Invalid item to find, must be a string or number"
invalidArgType = "Invalid argument type, first argument must be an array, object, or string"

[functions.copyIndex]
description = "Returns the current copy index"
invoked = "copyIndex function"
cannotUseOutsideCopy = "The 'copyIndex()' function can only be used when processing a 'Copy' loop"
loopNameNotFound = "The specified loop name '%{name}' was not found"
noCurrentLoop = "There is no current loop to get the index from"
offsetNegative = "The offset cannot be negative"

[functions.createArray]
description = "Creates an array from the given elements"
invoked = "createArray function"
Expand Down Expand Up @@ -412,6 +424,7 @@ argsMustBeStrings = "Arguments must all be strings"
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"

[functions.resourceId]
description = "Constructs a resource ID from the given type and name"
Expand Down
4 changes: 2 additions & 2 deletions dsc_lib/src/configure/config_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,11 +174,11 @@ pub enum CopyMode {
#[serde(deny_unknown_fields)]
pub struct Copy {
pub name: String,
pub count: i32,
pub count: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<CopyMode>,
#[serde(skip_serializing_if = "Option::is_none", rename = "batchSize")]
pub batch_size: Option<i32>,
pub batch_size: Option<i64>,
}

#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
Expand Down
44 changes: 30 additions & 14 deletions dsc_lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,56 @@ use std::{collections::HashMap, path::PathBuf};

use super::config_doc::{DataType, RestartRequired, SecurityContextKind};

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ProcessMode {
Copy,
Normal,
NoExpressionEvaluation,
ParametersDefault,
UserFunction,
}

#[derive(Clone)]
pub struct Context {
pub copy: HashMap<String, i64>,
pub copy_current_loop_name: String,
pub dsc_version: Option<String>,
pub execution_type: ExecutionKind,
pub extensions: Vec<DscExtension>,
pub references: Map<String, Value>,
pub system_root: PathBuf,
pub parameters: HashMap<String, (Value, DataType)>,
pub security_context: SecurityContextKind,
pub variables: Map<String, Value>,
pub start_datetime: DateTime<Local>,
pub restart_required: Option<Vec<RestartRequired>>,
pub process_expressions: bool,
pub process_mode: ProcessMode,
pub processing_parameter_defaults: bool,
pub dsc_version: Option<String>,
pub references: Map<String, Value>,
pub restart_required: Option<Vec<RestartRequired>>,
pub security_context: SecurityContextKind,
pub start_datetime: DateTime<Local>,
pub system_root: PathBuf,
pub variables: Map<String, Value>,
}

impl Context {
#[must_use]
pub fn new() -> Self {
Self {
copy: HashMap::new(),
copy_current_loop_name: String::new(),
dsc_version: None,
execution_type: ExecutionKind::Actual,
extensions: Vec::new(),
references: Map::new(),
system_root: get_default_os_system_root(),
parameters: HashMap::new(),
process_expressions: true,
process_mode: ProcessMode::Normal,
processing_parameter_defaults: false,
references: Map::new(),
restart_required: None,
security_context: match get_security_context() {
SecurityContext::Admin => SecurityContextKind::Elevated,
SecurityContext::User => SecurityContextKind::Restricted,
},
variables: Map::new(),
start_datetime: chrono::Local::now(),
restart_required: None,
process_expressions: true,
processing_parameter_defaults: false,
dsc_version: None,
system_root: get_default_os_system_root(),
variables: Map::new(),
}
}
}
Expand Down
33 changes: 31 additions & 2 deletions dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use crate::configure::config_doc::{ExecutionKind, Metadata, Resource};
use crate::configure::context::ProcessMode;
use crate::configure::{config_doc::RestartRequired, parameters::Input};
use crate::discovery::discovery_trait::DiscoveryFilter;
use crate::dscerror::DscError;
Expand Down Expand Up @@ -875,17 +876,45 @@ impl Configurator {
}

fn validate_config(&mut self) -> Result<(), DscError> {
let config: Configuration = serde_json::from_str(self.json.as_str())?;
let mut config: Configuration = serde_json::from_str(self.json.as_str())?;
check_security_context(config.metadata.as_ref())?;

// Perform discovery of resources used in config
// create an array of DiscoveryFilter using the resource types and api_versions from the config
let mut discovery_filter: Vec<DiscoveryFilter> = Vec::new();
for resource in &config.resources {
let config_copy = config.clone();
for resource in config_copy.resources {
let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.clone());
if !discovery_filter.contains(&filter) {
discovery_filter.push(filter);
}
// if the resource contains `Copy`, we need to unroll
if let Some(copy) = &resource.copy {
debug!("{}", t!("configure.mod.unrollingCopy", name = &copy.name, count = copy.count));
if copy.mode.is_some() {
return Err(DscError::Validation(t!("configure.mod.copyModeNotSupported").to_string()));
}
if copy.batch_size.is_some() {
return Err(DscError::Validation(t!("configure.mod.copyBatchSizeNotSupported").to_string()));
}
self.context.process_mode = ProcessMode::Copy;
self.context.copy_current_loop_name.clone_from(&copy.name);
let mut copy_resources = Vec::<Resource>::new();
for i in 0..copy.count {
self.context.copy.insert(copy.name.clone(), i);
let mut new_resource = resource.clone();
let Value::String(new_name) = self.statement_parser.parse_and_execute(&resource.name, &self.context)? else {
return Err(DscError::Parser(t!("configure.mod.copyNameResultNotString").to_string()))
};
new_resource.name = new_name.to_string();
new_resource.copy = None;
copy_resources.push(new_resource);
}
self.context.process_mode = ProcessMode::Normal;
// replace current resource with the unrolled copy resources
config.resources.retain(|r| *r != resource);
config.resources.extend(copy_resources);
}
}

self.discovery.find_resources(&discovery_filter, self.progress_format);
Expand Down
Loading
Loading