Skip to content

Commit aa6d647

Browse files
committed
Add support to validate expressions for names
1 parent 677faf3 commit aa6d647

File tree

4 files changed

+312
-0
lines changed

4 files changed

+312
-0
lines changed

dsc/src/subcommand.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,12 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, parame
381381
exit(EXIT_INVALID_INPUT);
382382
}
383383

384+
// process resource names after parameters are available
385+
if let Err(err) = configurator.process_resource_names() {
386+
error!("Error processing resource names: {err}");
387+
exit(EXIT_DSC_ERROR);
388+
}
389+
384390
match subcommand {
385391
ConfigSubCommand::Get { output_format, .. } => {
386392
config_get(&mut configurator, output_format.as_ref(), as_group);

dsc/tests/dsc_expressions.tests.ps1

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,287 @@ resources:
242242
$log | Should -BeLike "*ERROR* Arguments must be of the same type*"
243243

244244
}
245+
g
246+
Context 'Resource name expression evaluation' {
247+
It 'Simple parameter expression in resource name: <expression>' -TestCases @(
248+
@{ expression = "[parameters('resourceName')]"; paramValue = 'TestResource'; expected = 'TestResource' }
249+
@{ expression = "[parameters('serviceName')]"; paramValue = 'MyService'; expected = 'MyService' }
250+
) {
251+
param($expression, $paramValue, $expected)
252+
$yaml = @"
253+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
254+
parameters:
255+
resourceName:
256+
type: string
257+
defaultValue: $paramValue
258+
serviceName:
259+
type: string
260+
defaultValue: $paramValue
261+
resources:
262+
- name: "$expression"
263+
type: Microsoft/OSInfo
264+
properties: {}
265+
"@
266+
$out = .\dsc\target\debug\dsc.exe config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
267+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
268+
$out.results[0].name | Should -Be $expected
269+
}
270+
271+
It 'Concat function in resource name: <expression>' -TestCases @(
272+
@{ expression = "[concat('prefix-', parameters('name'))]"; paramValue = 'test'; expected = 'prefix-test' }
273+
@{ expression = "[concat(parameters('prefix'), '-', parameters('suffix'))]"; expected = 'start-end' }
274+
@{ expression = "[concat('Resource-', string(parameters('index')))]"; expected = 'Resource-42' }
275+
) {
276+
param($expression, $paramValue, $expected)
277+
$yaml = @"
278+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
279+
parameters:
280+
name:
281+
type: string
282+
defaultValue: ${paramValue}
283+
prefix:
284+
type: string
285+
defaultValue: start
286+
suffix:
287+
type: string
288+
defaultValue: end
289+
index:
290+
type: int
291+
defaultValue: 42
292+
resources:
293+
- name: "$expression"
294+
type: Microsoft/OSInfo
295+
properties: {}
296+
"@
297+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
298+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
299+
$out.results[0].name | Should -Be $expected
300+
}
301+
302+
It 'Format function in resource name: <expression>' -TestCases @(
303+
@{ expression = "[format('Service-{0}', parameters('id'))]"; expected = 'Service-123' }
304+
@{ expression = "[format('{0}-{1}-{2}', parameters('env'), parameters('app'), parameters('ver'))]"; expected = 'prod-web-v1' }
305+
@{ expression = "[format('Resource_{0:D3}', parameters('num'))]"; expected = 'Resource_005' }
306+
) {
307+
param($expression, $expected)
308+
$yaml = @"
309+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
310+
parameters:
311+
id:
312+
type: string
313+
defaultValue: '123'
314+
env:
315+
type: string
316+
defaultValue: prod
317+
app:
318+
type: string
319+
defaultValue: web
320+
ver:
321+
type: string
322+
defaultValue: v1
323+
num:
324+
type: int
325+
defaultValue: 5
326+
resources:
327+
- name: "$expression"
328+
type: Microsoft/OSInfo
329+
properties: {}
330+
"@
331+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
332+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
333+
$out.results[0].name | Should -Be $expected
334+
}
335+
336+
It 'Complex expression in resource name: <expression>' -TestCases @(
337+
@{ expression = "[concat(parameters('prefix'), '-', string(add(parameters('base'), parameters('offset'))))]"; expected = 'server-105' }
338+
@{ expression = "[format('{0}-{1}', parameters('type'), if(equals(parameters('env'), 'prod'), 'production', 'development'))]"; expected = 'web-production' }
339+
@{ expression = "[toLower(concat(parameters('region'), '-', parameters('service')))]"; expected = 'eastus-webapp' }
340+
) {
341+
param($expression, $expected)
342+
$yaml = @"
343+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
344+
parameters:
345+
prefix:
346+
type: string
347+
defaultValue: server
348+
base:
349+
type: int
350+
defaultValue: 100
351+
offset:
352+
type: int
353+
defaultValue: 5
354+
type:
355+
type: string
356+
defaultValue: web
357+
env:
358+
type: string
359+
defaultValue: prod
360+
region:
361+
type: string
362+
defaultValue: EASTUS
363+
service:
364+
type: string
365+
defaultValue: WebApp
366+
resources:
367+
- name: "$expression"
368+
type: Microsoft/OSInfo
369+
properties: {}
370+
"@
371+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
372+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
373+
$out.results[0].name | Should -Be $expected
374+
}
375+
376+
It 'Expression with object parameter access: <expression>' -TestCases @(
377+
@{ expression = "[parameters('config').name]"; expected = 'MyApp' }
378+
@{ expression = "[concat(parameters('config').prefix, '-', parameters('config').id)]"; expected = 'app-001' }
379+
@{ expression = "[parameters('servers')[0]]"; expected = 'web01' }
380+
@{ expression = "[parameters('servers')[parameters('config').index]]"; expected = 'db01' }
381+
) {
382+
param($expression, $expected)
383+
$yaml = @"
384+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
385+
parameters:
386+
config:
387+
type: object
388+
defaultValue:
389+
name: MyApp
390+
prefix: app
391+
id: '001'
392+
index: 1
393+
servers:
394+
type: array
395+
defaultValue:
396+
- web01
397+
- db01
398+
- cache01
399+
resources:
400+
- name: "$expression"
401+
type: Microsoft/OSInfo
402+
properties: {}
403+
"@
404+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
405+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
406+
$out.results[0].name | Should -Be $expected
407+
}
408+
409+
It 'Resource name expression error cases: <expression>' -TestCases @(
410+
@{ expression = "[parameters('nonexistent')]"; errorPattern = "*Parameter 'nonexistent' not found*" }
411+
@{ expression = "[concat()]"; errorPattern = "*requires at least 1 argument*" }
412+
@{ expression = "[add('text', 'more')]"; errorPattern = "*must be a number*" }
413+
@{ expression = "[parameters('config').nonexistent]"; errorPattern = "*Property 'nonexistent' not found*" }
414+
@{ expression = "[parameters('array')[10]]"; errorPattern = "*Index out of bounds*" }
415+
) {
416+
param($expression, $errorPattern)
417+
$yaml = @"
418+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
419+
parameters:
420+
config:
421+
type: object
422+
defaultValue:
423+
name: test
424+
array:
425+
type: array
426+
defaultValue:
427+
- item1
428+
- item2
429+
resources:
430+
- name: "$expression"
431+
type: Microsoft/OSInfo
432+
properties: {}
433+
"@
434+
dsc config get -i $yaml 2>$TestDrive/error.log | Out-Null
435+
$LASTEXITCODE | Should -Be 2
436+
$errorLog = Get-Content $TestDrive/error.log -Raw
437+
$errorLog | Should -BeLike $errorPattern
438+
}
439+
440+
It 'Resource name expression must evaluate to string' {
441+
$yaml = @'
442+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
443+
parameters:
444+
number:
445+
type: int
446+
defaultValue: 42
447+
resources:
448+
- name: "[parameters('number')]"
449+
type: Microsoft/OSInfo
450+
properties: {}
451+
'@
452+
dsc config get -i $yaml 2>$TestDrive/error.log | Out-Null
453+
$LASTEXITCODE | Should -Be 2
454+
$errorLog = Get-Content $TestDrive/error.log -Raw
455+
$errorLog | Should -BeLike "*Resource name expression must evaluate to a string*"
456+
}
457+
458+
It 'Multiple resources with different name expressions' {
459+
$yaml = @'
460+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
461+
parameters:
462+
env:
463+
type: string
464+
defaultValue: test
465+
appId:
466+
type: int
467+
defaultValue: 1
468+
resources:
469+
- name: "[concat('web-', parameters('env'))]"
470+
type: Microsoft/OSInfo
471+
properties: {}
472+
- name: "[format('app-{0:D2}', parameters('appId'))]"
473+
type: Microsoft/OSInfo
474+
properties: {}
475+
- name: "static-name"
476+
type: Microsoft/OSInfo
477+
properties: {}
478+
'@
479+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
480+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
481+
$out.results[0].name | Should -Be 'web-test'
482+
$out.results[1].name | Should -Be 'app-01'
483+
$out.results[2].name | Should -Be 'static-name'
484+
}
485+
486+
It 'Resource name expression with conditional logic' {
487+
$yaml = @'
488+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
489+
parameters:
490+
isProd:
491+
type: bool
492+
defaultValue: true
493+
serviceName:
494+
type: string
495+
defaultValue: api
496+
resources:
497+
- name: "[concat(parameters('serviceName'), if(parameters('isProd'), '-prod', '-dev'))]"
498+
type: Microsoft/OSInfo
499+
properties: {}
500+
'@
501+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
502+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
503+
$out.results[0].name | Should -Be 'api-prod'
504+
}
505+
506+
It 'Resource name with nested function calls' {
507+
$yaml = @'
508+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
509+
parameters:
510+
config:
511+
type: object
512+
defaultValue:
513+
services:
514+
- web
515+
- api
516+
- db
517+
selectedIndex: 1
518+
resources:
519+
- name: "[toUpper(parameters('config').services[parameters('config').selectedIndex])]"
520+
type: Microsoft/OSInfo
521+
properties: {}
522+
'@
523+
$out = dsc config get -i $yaml 2>$TestDrive/error.log | ConvertFrom-Json
524+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw | Out-String)
525+
$out.results[0].name | Should -Be 'API'
526+
}
527+
}
245528
}

dsc_lib/locales/en-us.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ unrollingCopy = "Unrolling copy for resource '%{name}' with count %{count}"
7676
copyModeNotSupported = "Copy mode is not supported"
7777
copyBatchSizeNotSupported = "Copy batch size is not supported"
7878
copyNameResultNotString = "Copy name result is not a string"
79+
nameResultNotString = "Resource name result is not a string"
7980
userFunctionAlreadyDefined = "User function '%{name}' in namespace '%{namespace}' is already defined"
8081
addingUserFunction = "Adding user function '%{name}'"
8182

dsc_lib/src/configure/mod.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -912,6 +912,28 @@ impl Configurator {
912912
Ok(())
913913
}
914914

915+
pub fn process_resource_names(&mut self) -> Result<(), DscError> {
916+
let mut config = self.config.clone();
917+
let config_copy = config.clone();
918+
919+
for resource in config_copy.resources {
920+
// skip resources that were created from copy loops (they already have evaluated names)
921+
if resource.copy.is_none() && resource.name.starts_with('[') && resource.name.ends_with(']') {
922+
// process resource name expressions for non-copy resources
923+
let Value::String(new_name) = self.statement_parser.parse_and_execute(&resource.name, &self.context)? else {
924+
return Err(DscError::Parser(t!("configure.mod.nameResultNotString").to_string()))
925+
};
926+
// find and update the resource name in the config
927+
if let Some(config_resource) = config.resources.iter_mut().find(|r| r.name == resource.name && r.resource_type == resource.resource_type) {
928+
config_resource.name = new_name.to_string();
929+
}
930+
}
931+
}
932+
933+
self.config = config;
934+
Ok(())
935+
}
936+
915937
fn invoke_property_expressions(&mut self, properties: Option<&Map<String, Value>>) -> Result<Option<Map<String, Value>>, DscError> {
916938
debug!("{}", t!("configure.mod.invokePropertyExpressions"));
917939
if properties.is_none() {

0 commit comments

Comments
 (0)