Skip to content

Commit bd569d9

Browse files
authored
Merge pull request #1077 from SteveL-MSFT/resource-versioning
Enable specifying version for resource
2 parents 74a652f + 001317a commit bd569d9

33 files changed

+734
-227
lines changed

dsc/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dsc/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ path-absolutize = { version = "3.1" }
2424
regex = "1.11"
2525
rust-i18n = { version = "3.1" }
2626
schemars = { version = "1.0" }
27+
semver = "1.0"
2728
serde = { version = "1.0", features = ["derive"] }
2829
serde_json = { version = "1.0", features = ["preserve_order"] }
2930
serde_yaml = { version = "0.9" }

dsc/examples/hello_world.dsc.bicep

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
targetScope = 'desiredStateConfiguration'
44

55
// use workaround where Bicep currently requires version in date format
6-
resource echo 'Microsoft.DSC.Debug/Echo@2025-01-01' = {
6+
resource echo 'Microsoft.DSC.Debug/Echo@2025-08-27' = {
77
name: 'exampleEcho'
88
properties: {
99
output: 'Hello, world!'

dsc/locales/en-us.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ getAll = "Get all instances of the resource"
3434
resource = "The name of the resource to invoke"
3535
functionAbout = "Operations on DSC functions"
3636
listFunctionAbout = "List or find functions"
37+
version = "The version of the resource to invoke in semver format"
3738

3839
[main]
3940
ctrlCReceived = "Ctrl-C received"

dsc/src/args.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ pub enum ResourceSubCommand {
215215
all: bool,
216216
#[clap(short, long, help = t!("args.resource").to_string())]
217217
resource: String,
218+
#[clap(short, long, help = t!("args.version").to_string())]
219+
version: Option<String>,
218220
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
219221
input: Option<String>,
220222
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
@@ -226,6 +228,8 @@ pub enum ResourceSubCommand {
226228
Set {
227229
#[clap(short, long, help = t!("args.resource").to_string())]
228230
resource: String,
231+
#[clap(short, long, help = t!("args.version").to_string())]
232+
version: Option<String>,
229233
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
230234
input: Option<String>,
231235
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
@@ -237,6 +241,8 @@ pub enum ResourceSubCommand {
237241
Test {
238242
#[clap(short, long, help = t!("args.resource").to_string())]
239243
resource: String,
244+
#[clap(short, long, help = t!("args.version").to_string())]
245+
version: Option<String>,
240246
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
241247
input: Option<String>,
242248
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
@@ -248,6 +254,8 @@ pub enum ResourceSubCommand {
248254
Delete {
249255
#[clap(short, long, help = t!("args.resource").to_string())]
250256
resource: String,
257+
#[clap(short, long, help = t!("args.version").to_string())]
258+
version: Option<String>,
251259
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
252260
input: Option<String>,
253261
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
@@ -257,13 +265,17 @@ pub enum ResourceSubCommand {
257265
Schema {
258266
#[clap(short, long, help = t!("args.resource").to_string())]
259267
resource: String,
268+
#[clap(short, long, help = t!("args.version").to_string())]
269+
version: Option<String>,
260270
#[clap(short = 'o', long, help = t!("args.outputFormat").to_string())]
261271
output_format: Option<OutputFormat>,
262272
},
263273
#[clap(name = "export", about = "Retrieve all resource instances", arg_required_else_help = true)]
264274
Export {
265275
#[clap(short, long, help = t!("args.resource").to_string())]
266276
resource: String,
277+
#[clap(short, long, help = t!("args.version").to_string())]
278+
version: Option<String>,
267279
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
268280
input: Option<String>,
269281
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]

dsc/src/resource_command.rs

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ use dsc_lib::{
1616
};
1717
use std::process::exit;
1818

19-
pub fn get(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&GetOutputFormat>) {
20-
let Some(resource) = get_resource(dsc, resource_type) else {
21-
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
19+
pub fn get(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&GetOutputFormat>) {
20+
let Some(resource) = get_resource(dsc, resource_type, version) else {
21+
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
2222
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
2323
};
2424

@@ -67,10 +67,10 @@ pub fn get(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&G
6767
}
6868
}
6969

70-
pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&GetOutputFormat>) {
70+
pub fn get_all(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, format: Option<&GetOutputFormat>) {
7171
let input = String::new();
72-
let Some(resource) = get_resource(dsc, resource_type) else {
73-
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
72+
let Some(resource) = get_resource(dsc, resource_type, version) else {
73+
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
7474
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
7575
};
7676

@@ -125,14 +125,14 @@ pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&GetOutputF
125125
}
126126
}
127127

128-
pub fn set(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) {
128+
pub fn set(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&OutputFormat>) {
129129
if input.is_empty() {
130130
error!("{}", t!("resource_command.setInputEmpty"));
131131
exit(EXIT_INVALID_ARGS);
132132
}
133133

134-
let Some(resource) = get_resource(dsc, resource_type) else {
135-
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
134+
let Some(resource) = get_resource(dsc, resource_type, version) else {
135+
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
136136
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
137137
};
138138

@@ -161,14 +161,14 @@ pub fn set(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&O
161161
}
162162
}
163163

164-
pub fn test(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) {
164+
pub fn test(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&OutputFormat>) {
165165
if input.is_empty() {
166166
error!("{}", t!("resource_command.testInputEmpty"));
167167
exit(EXIT_INVALID_ARGS);
168168
}
169169

170-
let Some(resource) = get_resource(dsc, resource_type) else {
171-
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
170+
let Some(resource) = get_resource(dsc, resource_type, version) else {
171+
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
172172
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
173173
};
174174

@@ -197,9 +197,9 @@ pub fn test(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&
197197
}
198198
}
199199

200-
pub fn delete(dsc: &DscManager, resource_type: &str, input: &str) {
201-
let Some(resource) = get_resource(dsc, resource_type) else {
202-
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
200+
pub fn delete(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str) {
201+
let Some(resource) = get_resource(dsc, resource_type, version) else {
202+
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
203203
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
204204
};
205205

@@ -218,9 +218,9 @@ pub fn delete(dsc: &DscManager, resource_type: &str, input: &str) {
218218
}
219219
}
220220

221-
pub fn schema(dsc: &DscManager, resource_type: &str, format: Option<&OutputFormat>) {
222-
let Some(resource) = get_resource(dsc, resource_type) else {
223-
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
221+
pub fn schema(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, format: Option<&OutputFormat>) {
222+
let Some(resource) = get_resource(dsc, resource_type, version) else {
223+
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
224224
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
225225
};
226226
if resource.kind == Kind::Adapter {
@@ -247,9 +247,9 @@ pub fn schema(dsc: &DscManager, resource_type: &str, format: Option<&OutputForma
247247
}
248248
}
249249

250-
pub fn export(dsc: &mut DscManager, resource_type: &str, input: &str, format: Option<&OutputFormat>) {
251-
let Some(dsc_resource) = get_resource(dsc, resource_type) else {
252-
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
250+
pub fn export(dsc: &mut DscManager, resource_type: &str, version: Option<&str>, input: &str, format: Option<&OutputFormat>) {
251+
let Some(dsc_resource) = get_resource(dsc, resource_type, version) else {
252+
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
253253
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
254254
};
255255

@@ -275,7 +275,7 @@ pub fn export(dsc: &mut DscManager, resource_type: &str, input: &str, format: Op
275275
}
276276

277277
#[must_use]
278-
pub fn get_resource<'a>(dsc: &'a DscManager, resource: &str) -> Option<&'a DscResource> {
278+
pub fn get_resource<'a>(dsc: &'a mut DscManager, resource: &str, version: Option<&str>) -> Option<&'a DscResource> {
279279
//TODO: add dynamically generated resource to dsc
280-
dsc.find_resource(resource)
280+
dsc.find_resource(resource, version)
281281
}

dsc/src/subcommand.rs

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use dsc_lib::{
1717
config_result::ResourceGetResult,
1818
Configurator,
1919
},
20-
discovery::discovery_trait::DiscoveryKind,
20+
discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind},
2121
discovery::command_discovery::ImportedManifest,
2222
dscerror::DscError,
2323
DscManager,
@@ -35,6 +35,7 @@ use dsc_lib::{
3535
};
3636
use regex::RegexBuilder;
3737
use rust_i18n::t;
38+
use core::convert::AsRef;
3839
use std::{
3940
collections::HashMap,
4041
io::{self, IsTerminal},
@@ -490,17 +491,12 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
490491
};
491492

492493
// discover the resources
493-
let mut resource_types = Vec::new();
494+
let mut resource_types = Vec::<DiscoveryFilter>::new();
494495
for resource_block in resources {
495496
let Some(type_name) = resource_block["type"].as_str() else {
496497
return Err(DscError::Validation(t!("subcommand.resourceTypeNotSpecified").to_string()));
497498
};
498-
499-
if resource_types.contains(&type_name.to_lowercase()) {
500-
continue;
501-
}
502-
503-
resource_types.push(type_name.to_lowercase().to_string());
499+
resource_types.push(DiscoveryFilter::new(type_name, resource_block["api_version"].as_str().map(std::string::ToString::to_string)));
504500
}
505501
dsc.find_resources(&resource_types, progress_format);
506502

@@ -512,7 +508,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
512508
trace!("{} '{}'", t!("subcommand.validatingResource"), resource_block["name"].as_str().unwrap_or_default());
513509

514510
// get the actual resource
515-
let Some(resource) = get_resource(&dsc, type_name) else {
511+
let Some(resource) = get_resource(&mut dsc, type_name, resource_block["api_version"].as_str()) else {
516512
return Err(DscError::Validation(format!("{}: '{type_name}'", t!("subcommand.resourceNotFound"))));
517513
};
518514

@@ -579,43 +575,43 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat
579575
ResourceSubCommand::List { resource_name, adapter_name, description, tags, output_format } => {
580576
list_resources(&mut dsc, resource_name.as_ref(), adapter_name.as_ref(), description.as_ref(), tags.as_ref(), output_format.as_ref(), progress_format);
581577
},
582-
ResourceSubCommand::Schema { resource , output_format } => {
583-
dsc.find_resources(&[resource.to_string()], progress_format);
584-
resource_command::schema(&dsc, resource, output_format.as_ref());
578+
ResourceSubCommand::Schema { resource , version, output_format } => {
579+
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
580+
resource_command::schema(&mut dsc, resource, version.as_deref(), output_format.as_ref());
585581
},
586-
ResourceSubCommand::Export { resource, input, file, output_format } => {
587-
dsc.find_resources(&[resource.to_string()], progress_format);
582+
ResourceSubCommand::Export { resource, version, input, file, output_format } => {
583+
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
588584
let parsed_input = get_input(input.as_ref(), file.as_ref(), false);
589-
resource_command::export(&mut dsc, resource, &parsed_input, output_format.as_ref());
585+
resource_command::export(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
590586
},
591-
ResourceSubCommand::Get { resource, input, file: path, all, output_format } => {
592-
dsc.find_resources(&[resource.to_string()], progress_format);
587+
ResourceSubCommand::Get { resource, version, input, file: path, all, output_format } => {
588+
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
593589
if *all {
594-
resource_command::get_all(&dsc, resource, output_format.as_ref());
590+
resource_command::get_all(&mut dsc, resource, version.as_deref(), output_format.as_ref());
595591
}
596592
else {
597593
if *output_format == Some(GetOutputFormat::JsonArray) {
598594
error!("{}", t!("subcommand.jsonArrayNotSupported"));
599595
exit(EXIT_INVALID_ARGS);
600596
}
601597
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
602-
resource_command::get(&dsc, resource, &parsed_input, output_format.as_ref());
598+
resource_command::get(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
603599
}
604600
},
605-
ResourceSubCommand::Set { resource, input, file: path, output_format } => {
606-
dsc.find_resources(&[resource.to_string()], progress_format);
601+
ResourceSubCommand::Set { resource, version, input, file: path, output_format } => {
602+
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
607603
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
608-
resource_command::set(&dsc, resource, &parsed_input, output_format.as_ref());
604+
resource_command::set(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
609605
},
610-
ResourceSubCommand::Test { resource, input, file: path, output_format } => {
611-
dsc.find_resources(&[resource.to_string()], progress_format);
606+
ResourceSubCommand::Test { resource, version, input, file: path, output_format } => {
607+
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
612608
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
613-
resource_command::test(&dsc, resource, &parsed_input, output_format.as_ref());
609+
resource_command::test(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
614610
},
615-
ResourceSubCommand::Delete { resource, input, file: path } => {
616-
dsc.find_resources(&[resource.to_string()], progress_format);
611+
ResourceSubCommand::Delete { resource, version, input, file: path } => {
612+
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
617613
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
618-
resource_command::delete(&dsc, resource, &parsed_input);
614+
resource_command::delete(&mut dsc, resource, version.as_deref(), &parsed_input);
619615
},
620616
}
621617
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Tests for resource versioning' {
5+
It "Should return the correct version '<version>' for operation '<operation>'" -TestCases @(
6+
@{ version = '1.1.2'; operation = 'get'; property = 'actualState' }
7+
@{ version = '1.1.0'; operation = 'get'; property = 'actualState' }
8+
@{ version = '2.0.0'; operation = 'get'; property = 'actualState' }
9+
@{ version = '1.1.2'; operation = 'set'; property = 'afterState' }
10+
@{ version = '1.1.0'; operation = 'set'; property = 'afterState' }
11+
@{ version = '2.0.0'; operation = 'set'; property = 'afterState' }
12+
@{ version = '1.1.2'; operation = 'test'; property = 'actualState' }
13+
@{ version = '1.1.0'; operation = 'test'; property = 'actualState' }
14+
@{ version = '2.0.0'; operation = 'test'; property = 'actualState' }
15+
) {
16+
param($version, $operation, $property)
17+
$config_yaml = @"
18+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
19+
resources:
20+
- name: Test Version
21+
type: Test/Version
22+
apiVersion: $version
23+
properties:
24+
version: $version
25+
"@
26+
$out = dsc -l trace config $operation -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json
27+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
28+
$out.results[0].result.$property.version | Should -BeExactly $version
29+
}
30+
31+
It "Version requirements '<req>' should return correct version" -TestCases @(
32+
@{ req = '>=1.0.0' ; expected = '2.0.0' }
33+
@{ req = '<=1.1.0' ; expected = '1.1.0' }
34+
@{ req = '<1.3' ; expected = '1.1.3' }
35+
@{ req = '>1,<=2.0.0' ; expected = '2.0.0' }
36+
@{ req = '>1.0.0,<2.0.0' ; expected = '1.1.3' }
37+
@{ req = '1'; expected = '1.1.3' }
38+
@{ req = '1.1' ; expected = '1.1.3' }
39+
@{ req = '^1.0' ; expected = '1.1.3' }
40+
@{ req = '~1.1' ; expected = '1.1.3' }
41+
@{ req = '*' ; expected = '2.0.0' }
42+
@{ req = '1.*' ; expected = '1.1.3' }
43+
@{ req = '2.1.0-preview.2' ; expected = '2.1.0-preview.2' }
44+
) {
45+
param($req, $expected)
46+
$config_yaml = @"
47+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
48+
resources:
49+
- name: Test Version
50+
type: Test/Version
51+
apiVersion: '$req'
52+
properties:
53+
version: $expected
54+
"@
55+
$out = dsc -l trace config test -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json
56+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
57+
$out.results[0].result.actualState.version | Should -BeExactly $expected
58+
}
59+
60+
It 'Multiple versions should be handled correctly' {
61+
$config_yaml = @"
62+
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
63+
resources:
64+
- name: Test Version 1
65+
type: Test/Version
66+
apiVersion: '1.1.2'
67+
- name: Test Version 2
68+
type: Test/Version
69+
apiVersion: '1.1.0'
70+
- name: Test Version 3
71+
type: Test/Version
72+
apiVersion: '2'
73+
"@
74+
$out = dsc -l trace config get -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json
75+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
76+
$out.results[0].result.actualState.version | Should -BeExactly '1.1.2'
77+
$out.results[1].result.actualState.version | Should -BeExactly '1.1.0'
78+
$out.results[2].result.actualState.version | Should -BeExactly '2.0.0'
79+
}
80+
}

0 commit comments

Comments
 (0)