Skip to content

Commit 5d7597c

Browse files
committed
Enhance copy loop functionality by allowing reference() usage in copy mode
1 parent 3f669c2 commit 5d7597c

File tree

5 files changed

+140
-9
lines changed

5 files changed

+140
-9
lines changed

dsc/tests/dsc_copy.tests.ps1

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,4 +334,112 @@ resources:
334334
$out.results[1].name | Should -Be 'Server-1'
335335
$out.results[1].result.actualState.output | Should -Be 'web-2'
336336
}
337+
338+
It 'Copy works with reference() to previous copy loop resource' {
339+
$configYaml = @'
340+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
341+
resources:
342+
- name: "[format('Policy-{0}', copyIndex())]"
343+
copy:
344+
name: policyLoop
345+
count: 2
346+
type: Microsoft.DSC.Debug/Echo
347+
properties:
348+
output: "[format('PolicyId-{0}', copyIndex())]"
349+
- name: "[format('Permission-{0}', copyIndex())]"
350+
copy:
351+
name: permissionLoop
352+
count: 2
353+
type: Microsoft.DSC.Debug/Echo
354+
properties:
355+
output: "[reference(resourceId('Microsoft.DSC.Debug/Echo', format('Policy-{0}', copyIndex()))).output]"
356+
dependsOn:
357+
- "[resourceId('Microsoft.DSC.Debug/Echo', format('Policy-{0}', copyIndex()))]"
358+
'@
359+
$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
360+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $testdrive/error.log -Raw | Out-String)
361+
$out.results.Count | Should -Be 4
362+
$out.results[0].name | Should -Be 'Policy-0'
363+
$out.results[0].result.actualState.output | Should -Be 'PolicyId-0'
364+
$out.results[1].name | Should -Be 'Policy-1'
365+
$out.results[1].result.actualState.output | Should -Be 'PolicyId-1'
366+
$out.results[2].name | Should -Be 'Permission-0'
367+
$out.results[2].result.actualState.output | Should -Be 'PolicyId-0'
368+
$out.results[3].name | Should -Be 'Permission-1'
369+
$out.results[3].result.actualState.output | Should -Be 'PolicyId-1'
370+
}
371+
372+
It 'Copy works with reference() accessing nested property from previous loop' {
373+
$configYaml = @'
374+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
375+
resources:
376+
- name: "[format('Source-{0}', copyIndex())]"
377+
copy:
378+
name: sourceLoop
379+
count: 2
380+
type: Microsoft.DSC.Debug/Echo
381+
properties:
382+
output: "[concat('Value-', string(copyIndex(100)))]"
383+
- name: "[format('Target-{0}', copyIndex())]"
384+
copy:
385+
name: targetLoop
386+
count: 2
387+
type: Microsoft.DSC.Debug/Echo
388+
properties:
389+
output: "[concat('Copied: ', reference(resourceId('Microsoft.DSC.Debug/Echo', format('Source-{0}', copyIndex()))).output)]"
390+
dependsOn:
391+
- "[resourceId('Microsoft.DSC.Debug/Echo', format('Source-{0}', copyIndex()))]"
392+
'@
393+
$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
394+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $testdrive/error.log -Raw | Out-String)
395+
$out.results.Count | Should -Be 4
396+
$out.results[0].name | Should -Be 'Source-0'
397+
$out.results[0].result.actualState.output | Should -Be 'Value-100'
398+
$out.results[1].name | Should -Be 'Source-1'
399+
$out.results[1].result.actualState.output | Should -Be 'Value-101'
400+
$out.results[2].name | Should -Be 'Target-0'
401+
$out.results[2].result.actualState.output | Should -Be 'Copied: Value-100'
402+
$out.results[3].name | Should -Be 'Target-1'
403+
$out.results[3].result.actualState.output | Should -Be 'Copied: Value-101'
404+
}
405+
406+
It 'Copy with multiple nested copyIndex() calls in reference()' {
407+
$configYaml = @'
408+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
409+
resources:
410+
- name: "[format('Primary-{0}', copyIndex())]"
411+
copy:
412+
name: primaryLoop
413+
count: 3
414+
type: Microsoft.DSC.Debug/Echo
415+
properties:
416+
output: "[format('Data-{0}', add(copyIndex(), 1000))]"
417+
- name: "[format('Secondary-{0}', copyIndex())]"
418+
copy:
419+
name: secondaryLoop
420+
count: 3
421+
type: Microsoft.DSC.Debug/Echo
422+
properties:
423+
output: "[format('From {0}: {1}', copyIndex(), reference(resourceId('Microsoft.DSC.Debug/Echo', format('Primary-{0}', copyIndex()))).output)]"
424+
dependsOn:
425+
- "[resourceId('Microsoft.DSC.Debug/Echo', format('Primary-{0}', copyIndex()))]"
426+
'@
427+
$out = dsc -l trace config get -i $configYaml 2>$testdrive/error.log | ConvertFrom-Json
428+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $testdrive/error.log -Raw | Out-String)
429+
$out.results.Count | Should -Be 6
430+
$out.results[0].name | Should -Be 'Primary-0'
431+
$out.results[0].result.actualState.output | Should -Be 'Data-1000'
432+
$out.results[1].name | Should -Be 'Primary-1'
433+
$out.results[1].result.actualState.output | Should -Be 'Data-1001'
434+
$out.results[2].name | Should -Be 'Primary-2'
435+
$out.results[2].result.actualState.output | Should -Be 'Data-1002'
436+
$out.results[3].name | Should -Be 'Secondary-0'
437+
$out.results[3].result.actualState.output | Should -Be 'From 0: Data-1000'
438+
$out.results[4].name | Should -Be 'Secondary-1'
439+
$out.results[4].result.actualState.output | Should -Be 'From 1: Data-1001'
440+
$out.results[5].name | Should -Be 'Secondary-2'
441+
$out.results[5].result.actualState.output | Should -Be 'From 2: Data-1002'
442+
}
443+
444+
337445
}

lib/dsc-lib/locales/en-us.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,6 @@ sumOverflow = "Sum of startIndex and count causes overflow"
513513
description = "Retrieves the output of a previously executed resource"
514514
invoked = "reference function"
515515
keyNotFound = "Invalid resourceId or resource has not executed yet: %{key}"
516-
cannotUseInCopyMode = "The 'reference()' function cannot be used when processing a 'Copy' loop"
517516
unavailableInUserFunction = "The 'reference()' function is not available in user-defined functions"
518517

519518
[functions.resourceId]

lib/dsc-lib/src/configure/mod.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -328,15 +328,33 @@ impl Configurator {
328328
}
329329

330330
fn get_properties(&mut self, resource: &Resource, resource_kind: &Kind) -> Result<Option<Map<String, Value>>, DscError> {
331-
match resource_kind {
331+
// Restore copy loop context from resource tags if present
332+
if let Some(tags) = &resource.tags {
333+
for (key, value) in tags {
334+
if let Some(loop_name) = key.strip_prefix("__dsc_copy_loop_") {
335+
if let Some(index) = value.as_i64() {
336+
self.context.copy.insert(loop_name.to_string(), index);
337+
self.context.copy_current_loop_name = loop_name.to_string();
338+
}
339+
}
340+
}
341+
}
342+
343+
let result = match resource_kind {
332344
Kind::Group => {
333345
// if Group resource, we leave it to the resource to handle expressions
334346
Ok(resource.properties.clone())
335347
},
336348
_ => {
337349
Ok(self.invoke_property_expressions(resource.properties.as_ref())?)
338350
},
339-
}
351+
};
352+
353+
// Clear copy context after property evaluation
354+
self.context.copy.clear();
355+
self.context.copy_current_loop_name.clear();
356+
357+
result
340358
}
341359

342360
/// Invoke the get operation on a resource.
@@ -1040,8 +1058,17 @@ impl Configurator {
10401058
};
10411059
new_resource.name = new_name.to_string();
10421060

1043-
if let Some(properties) = &resource.properties {
1044-
new_resource.properties = self.invoke_property_expressions(Some(properties))?;
1061+
// Keep properties as-is during copy unrolling
1062+
// They contain expressions that will be evaluated during resource execution
1063+
// This allows reference() and copyIndex() to work correctly
1064+
new_resource.properties = resource.properties.clone();
1065+
1066+
// Store copy loop metadata in tags for later retrieval during execution
1067+
if new_resource.tags.is_none() {
1068+
new_resource.tags = Some(Map::new());
1069+
}
1070+
if let Some(tags) = &mut new_resource.tags {
1071+
tags.insert(format!("__dsc_copy_loop_{}", copy.name), Value::Number(i.into()));
10451072
}
10461073

10471074
new_resource.copy = None;

lib/dsc-lib/src/functions/copy_index.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ impl Function for CopyIndex {
3030

3131
fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
3232
debug!("{}", t!("functions.copyIndex.invoked"));
33-
if context.process_mode != ProcessMode::Copy {
33+
if context.process_mode != ProcessMode::Copy && context.copy.is_empty() {
3434
return Err(DscError::Parser(t!("functions.copyIndex.cannotUseOutsideCopy").to_string()));
3535
}
3636
match args.len() {

lib/dsc-lib/src/functions/reference.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ impl Function for Reference {
3434
fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
3535
debug!("{}", t!("functions.reference.invoked"));
3636

37-
if context.process_mode == ProcessMode::Copy {
38-
return Err(DscError::Parser(t!("functions.reference.cannotUseInCopyMode").to_string()));
39-
}
4037
if context.process_mode == ProcessMode::UserFunction {
4138
return Err(DscError::Parser(t!("functions.reference.unavailableInUserFunction").to_string()));
4239
}

0 commit comments

Comments
 (0)