Skip to content

Commit f7e6e82

Browse files
authored
Merge pull request #1188 from SteveL-MSFT/resourceid-encode
Encode name part of `resourceId()` function to allow name to accept any characters
2 parents aa3ffc0 + 8d169db commit f7e6e82

File tree

6 files changed

+72
-54
lines changed

6 files changed

+72
-54
lines changed

dsc/tests/dsc_functions.tests.ps1

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Describe 'tests for function expressions' {
123123
@{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'))]"; expected = [pscustomobject]@{
124124
shared = [pscustomobject]@{ value = 42; flag = $true }
125125
level = 1
126-
}
126+
}
127127
}
128128
@{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject3'))]"; expected = [pscustomobject]@{ level = 1 } }
129129
@{ expression = "[intersection(parameters('nestedObject1'), parameters('nestedObject2'), parameters('nestedObject4'))]"; expected = [pscustomobject]@{ level = 1 } }
@@ -948,10 +948,10 @@ Describe 'tests for function expressions' {
948948
@{ testInput = ' ' }
949949
) {
950950
param($testInput)
951-
951+
952952
$expected = [Uri]::EscapeDataString($testInput)
953953
$expression = "[uriComponent('$($testInput -replace "'", "''")')]"
954-
954+
955955
$config_yaml = @"
956956
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
957957
resources:
@@ -963,13 +963,13 @@ Describe 'tests for function expressions' {
963963
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
964964
$out.results[0].result.actualState.output | Should -BeExactly $expected
965965
}
966-
966+
967967
It 'uriComponent function works with concat' {
968968
$input1 = 'hello'
969969
$input2 = ' '
970970
$input3 = 'world'
971971
$expected = [Uri]::EscapeDataString($input1 + $input2 + $input3)
972-
972+
973973
$config_yaml = @"
974974
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
975975
resources:
@@ -995,10 +995,10 @@ Describe 'tests for function expressions' {
995995
@{ testInput = '100%25' }
996996
) {
997997
param($testInput)
998-
998+
999999
$expected = [Uri]::UnescapeDataString($testInput)
10001000
$expression = "[uriComponentToString('$($testInput -replace "'", "''")')]"
1001-
1001+
10021002
$config_yaml = @"
10031003
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
10041004
resources:
@@ -1010,11 +1010,11 @@ Describe 'tests for function expressions' {
10101010
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
10111011
$out.results[0].result.actualState.output | Should -BeExactly $expected
10121012
}
1013-
1013+
10141014
It 'uriComponentToString function works with round-trip encoding' {
10151015
$original = 'hello world'
10161016
$expected = $original
1017-
1017+
10181018
$config_yaml = @"
10191019
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
10201020
resources:
@@ -1026,11 +1026,11 @@ Describe 'tests for function expressions' {
10261026
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
10271027
$out.results[0].result.actualState.output | Should -BeExactly $expected
10281028
}
1029-
1029+
10301030
It 'uriComponentToString function works with nested round-trip' {
10311031
$original = '[email protected]'
10321032
$expected = $original
1033-
1033+
10341034
$config_yaml = @"
10351035
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
10361036
resources:
@@ -1042,13 +1042,13 @@ Describe 'tests for function expressions' {
10421042
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
10431043
$out.results[0].result.actualState.output | Should -BeExactly $expected
10441044
}
1045-
1045+
10461046
It 'uriComponentToString function works with concat' {
10471047
$input1 = 'hello'
10481048
$input2 = '%20'
10491049
$input3 = 'world'
10501050
$expected = [Uri]::UnescapeDataString($input1 + $input2 + $input3)
1051-
1051+
10521052
$config_yaml = @"
10531053
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
10541054
resources:
@@ -1077,7 +1077,7 @@ Describe 'tests for function expressions' {
10771077
@{ data = @{ nested = @{ value = 123 } }; accessor = '.nested.value'; expected = 123 }
10781078
) {
10791079
param($data, $accessor, $expected)
1080-
1080+
10811081
$jsonString = ConvertTo-Json -Compress -InputObject $data
10821082
$expression = "[json(''$($jsonString)'')$accessor]"
10831083

@@ -1152,7 +1152,7 @@ Describe 'tests for function expressions' {
11521152
@{ base = 'http://192.168.1.1/'; relative = 'api/v1'; expected = 'http://192.168.1.1/api/v1' }
11531153
) {
11541154
param($base, $relative, $expected)
1155-
1155+
11561156
$expression = "[uri('$($base -replace "'", "''")','$($relative -replace "'", "''")')]"
11571157
$config_yaml = @"
11581158
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
@@ -1188,4 +1188,20 @@ Describe 'tests for function expressions' {
11881188
$errorContent = Get-Content $TestDrive/error.log -Raw
11891189
$errorContent | Should -Match ([regex]::Escape($expectedError))
11901190
}
1191+
1192+
It 'resourceId allows for arbitrary characters in names including unicode' {
1193+
$name = 'My Resource @123/!#$%^&*()[]{}-+=;`~'
1194+
$config_yaml = @"
1195+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
1196+
resources:
1197+
- name: "$name"
1198+
type: Microsoft.DSC.Debug/Echo
1199+
properties:
1200+
output: "[resourceId('Microsoft.DSC.Debug/Echo', '$name')]"
1201+
"@
1202+
$out = dsc config get -i $config_yaml | ConvertFrom-Json
1203+
$LASTEXITCODE | Should -Be 0
1204+
$expected = "Microsoft.DSC.Debug/Echo:$([Uri]::EscapeDataString($name))"
1205+
$out.results[0].result.actualState.output | Should -BeExactly $expected
1206+
}
11911207
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -476,9 +476,6 @@ unavailableInUserFunction = "The 'reference()' function is not available in user
476476
[functions.resourceId]
477477
description = "Constructs a resource ID from the given type and name"
478478
incorrectTypeFormat = "Type argument must contain exactly one slash"
479-
invalidFirstArgType = "Invalid argument type for first parameter"
480-
incorrectNameFormat = "Name argument cannot contain a slash"
481-
invalidSecondArgType = "Invalid argument type for second parameter"
482479

483480
[functions.secret]
484481
description = "Retrieves a secret from a vault"

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statem
4242
let (resource_type, resource_name) = get_type_and_name(string_result)?;
4343

4444
// find the resource by name
45-
let Some(dependency_resource) = config.resources.iter().find(|r| r.name.eq(resource_name)) else {
45+
let Some(dependency_resource) = config.resources.iter().find(|r| r.name.eq(&resource_name)) else {
4646
return Err(DscError::Validation(t!("configure.dependsOn.dependencyNotFound", dependency_name = resource_name, resource_name = resource.name).to_string()));
4747
};
4848
// validate the type matches
@@ -91,12 +91,14 @@ pub fn get_resource_invocation_order(config: &Configuration, parser: &mut Statem
9191
Ok(order)
9292
}
9393

94-
fn get_type_and_name(statement: &str) -> Result<(&str, &str), DscError> {
94+
fn get_type_and_name(statement: &str) -> Result<(&str, String), DscError> {
9595
let parts: Vec<&str> = statement.split(':').collect();
9696
if parts.len() != 2 {
9797
return Err(DscError::Validation(t!("configure.dependsOn.syntaxIncorrect", dependency = statement).to_string()));
9898
}
99-
Ok((parts[0], parts[1]))
99+
// the name is url encoded so we need to decode it
100+
let decoded_name = urlencoding::decode(parts[1]).map_err(|_| DscError::Validation(t!("configure.dependsOn.syntaxIncorrect", dependency = statement).to_string()))?;
101+
Ok((parts[0], decoded_name.into_owned()))
100102
}
101103

102104
#[cfg(test)]

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::DscResource;
1515
use crate::discovery::Discovery;
1616
use crate::parser::Statement;
1717
use crate::progress::{Failure, ProgressBar, ProgressFormat};
18+
use crate::util::resource_id;
1819
use self::config_doc::{Configuration, DataType, MicrosoftDscMetadata, Operation, SecurityContextKind};
1920
use self::depends_on::get_resource_invocation_order;
2021
use self::config_result::{ConfigurationExportResult, ConfigurationGetResult, ConfigurationSetResult, ConfigurationTestResult};
@@ -388,15 +389,15 @@ impl Configurator {
388389

389390
match &mut get_result {
390391
GetResult::Resource(ref mut resource_result) => {
391-
self.context.references.insert(format!("{}:{}", resource.resource_type, evaluated_name), serde_json::to_value(&resource_result.actual_state)?);
392+
self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), serde_json::to_value(&resource_result.actual_state)?);
392393
get_metadata_from_result(Some(&mut self.context), &mut resource_result.actual_state, &mut metadata)?;
393394
},
394395
GetResult::Group(group) => {
395396
let mut results = Vec::<Value>::new();
396397
for result in group {
397398
results.push(serde_json::to_value(&result.result)?);
398399
}
399-
self.context.references.insert(format!("{}:{}", resource.resource_type, evaluated_name), Value::Array(results.clone()));
400+
self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), Value::Array(results.clone()));
400401
},
401402
}
402403
let resource_result = config_result::ResourceGetResult {
@@ -559,15 +560,15 @@ impl Configurator {
559560
};
560561
match &mut set_result {
561562
SetResult::Resource(resource_result) => {
562-
self.context.references.insert(format!("{}:{}", resource.resource_type, evaluated_name), serde_json::to_value(&resource_result.after_state)?);
563+
self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), serde_json::to_value(&resource_result.after_state)?);
563564
get_metadata_from_result(Some(&mut self.context), &mut resource_result.after_state, &mut metadata)?;
564565
},
565566
SetResult::Group(group) => {
566567
let mut results = Vec::<Value>::new();
567568
for result in group {
568569
results.push(serde_json::to_value(&result.result)?);
569570
}
570-
self.context.references.insert(format!("{}:{}", resource.resource_type, evaluated_name), Value::Array(results.clone()));
571+
self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), Value::Array(results.clone()));
571572
},
572573
}
573574
let resource_result = config_result::ResourceSetResult {
@@ -637,15 +638,15 @@ impl Configurator {
637638
};
638639
match &mut test_result {
639640
TestResult::Resource(resource_test_result) => {
640-
self.context.references.insert(format!("{}:{}", resource.resource_type, evaluated_name), serde_json::to_value(&resource_test_result.actual_state)?);
641+
self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), serde_json::to_value(&resource_test_result.actual_state)?);
641642
get_metadata_from_result(Some(&mut self.context), &mut resource_test_result.actual_state, &mut metadata)?;
642643
},
643644
TestResult::Group(group) => {
644645
let mut results = Vec::<Value>::new();
645646
for result in group {
646647
results.push(serde_json::to_value(&result.result)?);
647648
}
648-
self.context.references.insert(format!("{}:{}", resource.resource_type, evaluated_name), Value::Array(results.clone()));
649+
self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), Value::Array(results.clone()));
649650
},
650651
}
651652
let resource_result = config_result::ResourceTestResult {
@@ -707,7 +708,7 @@ impl Configurator {
707708
return Err(e);
708709
},
709710
};
710-
self.context.references.insert(format!("{}:{}", resource.resource_type, evaluated_name), serde_json::to_value(&export_result.actual_state)?);
711+
self.context.references.insert(resource_id(&resource.resource_type, &evaluated_name), serde_json::to_value(&export_result.actual_state)?);
711712
progress.set_result(&serde_json::to_value(export_result)?);
712713
progress.write_increment(1);
713714
}

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

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use crate::DscError;
55
use crate::configure::context::Context;
66
use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata};
7+
use crate::util::resource_id;
78
use rust_i18n::t;
89
use serde_json::Value;
910

@@ -28,32 +29,15 @@ impl Function for ResourceId {
2829
}
2930

3031
fn invoke(&self, args: &[Value], _context: &Context) -> Result<Value, DscError> {
31-
let mut result = String::new();
3232
// first argument is the type and must contain only 1 slash
33-
let resource_type = &args[0];
34-
if let Some(value) = resource_type.as_str() {
35-
let slash_count = value.chars().filter(|c| *c == '/').count();
36-
if slash_count != 1 {
37-
return Err(DscError::Function("resourceId".to_string(), t!("functions.resourceId.incorrectTypeFormat").to_string()));
38-
}
39-
result.push_str(value);
40-
} else {
41-
return Err(DscError::Parser(t!("functions.resourceId.invalidFirstArgType").to_string()));
42-
}
43-
// ARM uses a slash separator, but here we use a colon which is not allowed for the type nor name
44-
result.push(':');
45-
// second argument is the name and must contain no slashes
46-
let resource_name = &args[1];
47-
if let Some(value) = resource_name.as_str() {
48-
if value.contains('/') {
49-
return Err(DscError::Function("resourceId".to_string(), t!("functions.resourceId.incorrectNameFormat").to_string()));
50-
}
51-
52-
result.push_str(value);
53-
} else {
54-
return Err(DscError::Parser(t!("functions.resourceId.invalidSecondArgType").to_string()));
33+
let resource_type = &args[0].as_str().unwrap();
34+
let slash_count = resource_type.chars().filter(|c| *c == '/').count();
35+
if slash_count != 1 {
36+
return Err(DscError::Function("resourceId".to_string(), t!("functions.resourceId.incorrectTypeFormat").to_string()));
5537
}
5638

39+
let resource_name = &args[1].as_str().unwrap();
40+
let result = resource_id(resource_type, resource_name);
5741
Ok(Value::String(result))
5842
}
5943
}
@@ -85,10 +69,10 @@ mod tests {
8569
}
8670

8771
#[test]
88-
fn invalid_name() {
72+
fn valid_name_with_slashes() {
8973
let mut parser = Statement::new().unwrap();
90-
let result = parser.parse_and_execute("[resourceId('a','b/c')]", &Context::new());
91-
assert!(result.is_err());
74+
let result = parser.parse_and_execute("[resourceId('a/a','b/c/d')]", &Context::new()).unwrap();
75+
assert_eq!(result, "a/a:b%2Fc%2Fd");
9276
}
9377

9478
#[test]

lib/dsc-lib/src/util.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,24 @@ fn get_settings_policy_file_path() -> String
213213
Path::new("/etc").join("dsc").join("dsc.settings.json").display().to_string()
214214
}
215215

216+
/// Generates a resource ID from the specified type and name.
217+
///
218+
/// # Arguments
219+
/// * `type_name` - The resource type in the format "namespace/type".
220+
/// * `name` - The resource name.
221+
///
222+
/// # Returns
223+
/// A string that holds the resource ID in the format "namespace/type:name".
224+
#[must_use]
225+
pub fn resource_id(type_name: &str, name: &str) -> String {
226+
let mut result = String::new();
227+
result.push_str(type_name);
228+
result.push(':');
229+
let encoded = urlencoding::encode(name);
230+
result.push_str(&encoded);
231+
result
232+
}
233+
216234
#[macro_export]
217235
macro_rules! locked_is_empty {
218236
($lockable:expr) => {{

0 commit comments

Comments
 (0)