Skip to content

Commit 1cc08d6

Browse files
authored
Merge pull request #1099 from SteveL-MSFT/copy-loop
Add `Copy` support for resources
2 parents 901c5cf + b581e63 commit 1cc08d6

File tree

8 files changed

+353
-19
lines changed

8 files changed

+353
-19
lines changed

dsc/tests/dsc_copy.tests.ps1

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Tests for copy loops' {
5+
It 'Works for resources' {
6+
$configYaml = @'
7+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
8+
resources:
9+
- name: "[format('Test-{0}', copyIndex())]"
10+
copy:
11+
name: testLoop
12+
count: 3
13+
type: Microsoft.DSC.Debug/Echo
14+
properties:
15+
output: Hello
16+
'@
17+
$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
18+
$LASTEXITCODE | Should -Be 0 -Because ((Get-Content $testdrive/error.log) | Out-String)
19+
$out.results.Count | Should -Be 3
20+
$out.results[0].name | Should -Be 'Test-0'
21+
$out.results[0].result.actualState.output | Should -Be 'Hello'
22+
$out.results[1].name | Should -Be 'Test-1'
23+
$out.results[1].result.actualState.output | Should -Be 'Hello'
24+
$out.results[2].name | Should -Be 'Test-2'
25+
$out.results[2].result.actualState.output | Should -Be 'Hello'
26+
}
27+
28+
It 'copyIndex() works with offset' {
29+
$configYaml = @'
30+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
31+
resources:
32+
- name: "[format('Test-{0}', copyIndex(10))]"
33+
copy:
34+
name: testLoop
35+
count: 3
36+
type: Microsoft.DSC.Debug/Echo
37+
properties:
38+
output: Hello
39+
'@
40+
41+
$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
42+
$LASTEXITCODE | Should -Be 0 -Because ((Get-Content $testdrive/error.log) | Out-String)
43+
$out.results.Count | Should -Be 3
44+
$out.results[0].name | Should -Be 'Test-10'
45+
$out.results[0].result.actualState.output | Should -Be 'Hello'
46+
$out.results[1].name | Should -Be 'Test-11'
47+
$out.results[1].result.actualState.output | Should -Be 'Hello'
48+
$out.results[2].name | Should -Be 'Test-12'
49+
$out.results[2].result.actualState.output | Should -Be 'Hello'
50+
}
51+
52+
It 'copyIndex() with negative index returns error' {
53+
$configYaml = @'
54+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
55+
resources:
56+
- name: "[format('Test-{0}', copyIndex(-1))]"
57+
copy:
58+
name: testLoop
59+
count: 3
60+
type: Microsoft.DSC.Debug/Echo
61+
properties:
62+
output: Hello
63+
'@
64+
65+
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
66+
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
67+
(Get-Content $testdrive/error.log -Raw) | Should -Match 'The offset cannot be negative'
68+
}
69+
70+
It 'Copy works with count 0' {
71+
$configYaml = @'
72+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
73+
resources:
74+
- name: "[format('Test-{0}', copyIndex())]"
75+
copy:
76+
name: testLoop
77+
count: 0
78+
type: Microsoft.DSC.Debug/Echo
79+
properties:
80+
output: Hello
81+
'@
82+
83+
$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
84+
$LASTEXITCODE | Should -Be 0 -Because ((Get-Content $testdrive/error.log) | Out-String)
85+
$out.results.Count | Should -Be 0
86+
}
87+
88+
It 'copyIndex() with loop name works' {
89+
$configYaml = @'
90+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
91+
resources:
92+
- name: "[format('Test-{0}', copyIndex('testLoop'))]"
93+
copy:
94+
name: testLoop
95+
count: 3
96+
type: Microsoft.DSC.Debug/Echo
97+
properties:
98+
output: Hello
99+
'@
100+
$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
101+
$LASTEXITCODE | Should -Be 0 -Because ((Get-Content $testdrive/error.log) | Out-String)
102+
$out.results.Count | Should -Be 3
103+
$out.results[0].name | Should -Be 'Test-0'
104+
$out.results[0].result.actualState.output | Should -Be 'Hello'
105+
$out.results[1].name | Should -Be 'Test-1'
106+
$out.results[1].result.actualState.output | Should -Be 'Hello'
107+
$out.results[2].name | Should -Be 'Test-2'
108+
$out.results[2].result.actualState.output | Should -Be 'Hello'
109+
}
110+
111+
It 'copyIndex() with invalid loop name "<name>" returns error' -TestCases @(
112+
@{ name = "'noSuchLoop'" }
113+
@{ name = "'noSuchLoop', 1" }
114+
){
115+
param($name)
116+
$configYaml = @"
117+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
118+
resources:
119+
- name: "[format('Test-{0}', copyIndex($name))]"
120+
copy:
121+
name: testLoop
122+
count: 3
123+
type: Microsoft.DSC.Debug/Echo
124+
properties:
125+
output: Hello
126+
"@
127+
128+
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
129+
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
130+
(Get-Content $testdrive/error.log -Raw) | Should -Match "The specified loop name 'noSuchLoop' was not found"
131+
}
132+
133+
It 'Copy mode is not supported' {
134+
$configYaml = @'
135+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
136+
resources:
137+
- name: "[format('Test-{0}', copyIndex())]"
138+
copy:
139+
name: testLoop
140+
count: 3
141+
mode: serial
142+
type: Microsoft.DSC.Debug/Echo
143+
properties:
144+
output: Hello
145+
'@
146+
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
147+
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
148+
(Get-Content $testdrive/error.log -Raw) | Should -Match "Copy mode is not supported"
149+
}
150+
151+
It 'Copy batch size is not supported' {
152+
$configYaml = @'
153+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
154+
resources:
155+
- name: "[format('Test-{0}', copyIndex())]"
156+
copy:
157+
name: testLoop
158+
count: 3
159+
batchSize: 2
160+
type: Microsoft.DSC.Debug/Echo
161+
properties:
162+
output: Hello
163+
'@
164+
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
165+
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
166+
(Get-Content $testdrive/error.log -Raw) | Should -Match "Copy batch size is not supported"
167+
}
168+
169+
It 'Name expression during copy must be a string' {
170+
$configYaml = @'
171+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
172+
resources:
173+
- name: "[copyIndex()]"
174+
copy:
175+
name: testLoop
176+
count: 3
177+
type: Microsoft.DSC.Debug/Echo
178+
properties:
179+
output: Hello
180+
'@
181+
$null = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
182+
$LASTEXITCODE | Should -Be 2 -Because ((Get-Content $testdrive/error.log) | Out-String)
183+
(Get-Content $testdrive/error.log -Raw) | Should -Match "Copy name result is not a string"
184+
}
185+
}

dsc_lib/locales/en-us.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ metadataMicrosoftDscIgnored = "Resource returned '_metadata' property 'Microsoft
7272
metadataNotObject = "Resource returned '_metadata' property which is not an object"
7373
metadataRestartRequiredInvalid = "Resource returned '_metadata' property '_restartRequired' which contains invalid value: %{value}"
7474
schemaExcludesMetadata = "Will not add '_metadata' to properties because resource schema does not support it"
75+
unrollingCopy = "Unrolling copy for resource '%{name}' with count %{count}"
76+
copyModeNotSupported = "Copy mode is not supported"
77+
copyBatchSizeNotSupported = "Copy batch size is not supported"
78+
copyNameResultNotString = "Copy name result is not a string"
7579

7680
[discovery.commandDiscovery]
7781
couldNotReadSetting = "Could not read 'resourcePath' setting"
@@ -260,6 +264,14 @@ invoked = "contains function"
260264
invalidItemToFind = "Invalid item to find, must be a string or number"
261265
invalidArgType = "Invalid argument type, first argument must be an array, object, or string"
262266

267+
[functions.copyIndex]
268+
description = "Returns the current copy index"
269+
invoked = "copyIndex function"
270+
cannotUseOutsideCopy = "The 'copyIndex()' function can only be used when processing a 'Copy' loop"
271+
loopNameNotFound = "The specified loop name '%{name}' was not found"
272+
noCurrentLoop = "There is no current loop to get the index from"
273+
offsetNegative = "The offset cannot be negative"
274+
263275
[functions.createArray]
264276
description = "Creates an array from the given elements"
265277
invoked = "createArray function"
@@ -413,6 +425,7 @@ argsMustBeStrings = "Arguments must all be strings"
413425
description = "Retrieves the output of a previously executed resource"
414426
invoked = "reference function"
415427
keyNotFound = "Invalid resourceId or resource has not executed yet: %{key}"
428+
cannotUseInCopyMode = "The 'reference()' function cannot be used when processing a 'Copy' loop"
416429

417430
[functions.resourceId]
418431
description = "Constructs a resource ID from the given type and name"

dsc_lib/src/configure/config_doc.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,11 @@ pub enum CopyMode {
174174
#[serde(deny_unknown_fields)]
175175
pub struct Copy {
176176
pub name: String,
177-
pub count: i32,
177+
pub count: i64,
178178
#[serde(skip_serializing_if = "Option::is_none")]
179179
pub mode: Option<CopyMode>,
180180
#[serde(skip_serializing_if = "Option::is_none", rename = "batchSize")]
181-
pub batch_size: Option<i32>,
181+
pub batch_size: Option<i64>,
182182
}
183183

184184
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]

dsc_lib/src/configure/context.rs

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,40 +9,56 @@ use std::{collections::HashMap, path::PathBuf};
99

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

12+
#[derive(Debug, Clone, Eq, PartialEq)]
13+
pub enum ProcessMode {
14+
Copy,
15+
Normal,
16+
NoExpressionEvaluation,
17+
ParametersDefault,
18+
UserFunction,
19+
}
20+
21+
#[derive(Clone)]
1222
pub struct Context {
23+
pub copy: HashMap<String, i64>,
24+
pub copy_current_loop_name: String,
25+
pub dsc_version: Option<String>,
1326
pub execution_type: ExecutionKind,
1427
pub extensions: Vec<DscExtension>,
15-
pub references: Map<String, Value>,
16-
pub system_root: PathBuf,
1728
pub parameters: HashMap<String, (Value, DataType)>,
18-
pub security_context: SecurityContextKind,
19-
pub variables: Map<String, Value>,
20-
pub start_datetime: DateTime<Local>,
21-
pub restart_required: Option<Vec<RestartRequired>>,
2229
pub process_expressions: bool,
30+
pub process_mode: ProcessMode,
2331
pub processing_parameter_defaults: bool,
24-
pub dsc_version: Option<String>,
32+
pub references: Map<String, Value>,
33+
pub restart_required: Option<Vec<RestartRequired>>,
34+
pub security_context: SecurityContextKind,
35+
pub start_datetime: DateTime<Local>,
36+
pub system_root: PathBuf,
37+
pub variables: Map<String, Value>,
2538
}
2639

2740
impl Context {
2841
#[must_use]
2942
pub fn new() -> Self {
3043
Self {
44+
copy: HashMap::new(),
45+
copy_current_loop_name: String::new(),
46+
dsc_version: None,
3147
execution_type: ExecutionKind::Actual,
3248
extensions: Vec::new(),
33-
references: Map::new(),
34-
system_root: get_default_os_system_root(),
3549
parameters: HashMap::new(),
50+
process_expressions: true,
51+
process_mode: ProcessMode::Normal,
52+
processing_parameter_defaults: false,
53+
references: Map::new(),
54+
restart_required: None,
3655
security_context: match get_security_context() {
3756
SecurityContext::Admin => SecurityContextKind::Elevated,
3857
SecurityContext::User => SecurityContextKind::Restricted,
3958
},
40-
variables: Map::new(),
4159
start_datetime: chrono::Local::now(),
42-
restart_required: None,
43-
process_expressions: true,
44-
processing_parameter_defaults: false,
45-
dsc_version: None,
60+
system_root: get_default_os_system_root(),
61+
variables: Map::new(),
4662
}
4763
}
4864
}

dsc_lib/src/configure/mod.rs

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
use crate::configure::config_doc::{ExecutionKind, Metadata, Resource};
5+
use crate::configure::context::ProcessMode;
56
use crate::configure::{config_doc::RestartRequired, parameters::Input};
67
use crate::discovery::discovery_trait::DiscoveryFilter;
78
use crate::dscerror::DscError;
@@ -880,17 +881,45 @@ impl Configurator {
880881
}
881882

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

886887
// Perform discovery of resources used in config
887888
// create an array of DiscoveryFilter using the resource types and api_versions from the config
888889
let mut discovery_filter: Vec<DiscoveryFilter> = Vec::new();
889-
for resource in &config.resources {
890+
let config_copy = config.clone();
891+
for resource in config_copy.resources {
890892
let filter = DiscoveryFilter::new(&resource.resource_type, resource.api_version.clone());
891893
if !discovery_filter.contains(&filter) {
892894
discovery_filter.push(filter);
893895
}
896+
// if the resource contains `Copy`, we need to unroll
897+
if let Some(copy) = &resource.copy {
898+
debug!("{}", t!("configure.mod.unrollingCopy", name = &copy.name, count = copy.count));
899+
if copy.mode.is_some() {
900+
return Err(DscError::Validation(t!("configure.mod.copyModeNotSupported").to_string()));
901+
}
902+
if copy.batch_size.is_some() {
903+
return Err(DscError::Validation(t!("configure.mod.copyBatchSizeNotSupported").to_string()));
904+
}
905+
self.context.process_mode = ProcessMode::Copy;
906+
self.context.copy_current_loop_name.clone_from(&copy.name);
907+
let mut copy_resources = Vec::<Resource>::new();
908+
for i in 0..copy.count {
909+
self.context.copy.insert(copy.name.clone(), i);
910+
let mut new_resource = resource.clone();
911+
let Value::String(new_name) = self.statement_parser.parse_and_execute(&resource.name, &self.context)? else {
912+
return Err(DscError::Parser(t!("configure.mod.copyNameResultNotString").to_string()))
913+
};
914+
new_resource.name = new_name.to_string();
915+
new_resource.copy = None;
916+
copy_resources.push(new_resource);
917+
}
918+
self.context.process_mode = ProcessMode::Normal;
919+
// replace current resource with the unrolled copy resources
920+
config.resources.retain(|r| *r != resource);
921+
config.resources.extend(copy_resources);
922+
}
894923
}
895924

896925
self.discovery.find_resources(&discovery_filter, self.progress_format);

0 commit comments

Comments
 (0)