Skip to content

Commit 2664f57

Browse files
committed
Enable specifying semver and range for resources
1 parent 1f01635 commit 2664f57

23 files changed

+586
-200
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/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},
@@ -488,17 +489,12 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
488489
};
489490

490491
// discover the resources
491-
let mut resource_types = Vec::new();
492+
let mut resource_types = Vec::<DiscoveryFilter>::new();
492493
for resource_block in resources {
493494
let Some(type_name) = resource_block["type"].as_str() else {
494495
return Err(DscError::Validation(t!("subcommand.resourceTypeNotSpecified").to_string()));
495496
};
496-
497-
if resource_types.contains(&type_name.to_lowercase()) {
498-
continue;
499-
}
500-
501-
resource_types.push(type_name.to_lowercase().to_string());
497+
resource_types.push(DiscoveryFilter::new(type_name.to_lowercase().to_string(), resource_block["api_version"].as_str().map(|s| s.to_string())));
502498
}
503499
dsc.find_resources(&resource_types, progress_format);
504500

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

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

@@ -577,43 +573,43 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat
577573
ResourceSubCommand::List { resource_name, adapter_name, description, tags, output_format } => {
578574
list_resources(&mut dsc, resource_name.as_ref(), adapter_name.as_ref(), description.as_ref(), tags.as_ref(), output_format.as_ref(), progress_format);
579575
},
580-
ResourceSubCommand::Schema { resource , output_format } => {
581-
dsc.find_resources(&[resource.to_string()], progress_format);
582-
resource_command::schema(&dsc, resource, output_format.as_ref());
576+
ResourceSubCommand::Schema { resource , version, output_format } => {
577+
dsc.find_resources(&[DiscoveryFilter::new(resource.clone(), version.clone())], progress_format);
578+
resource_command::schema(&mut dsc, resource, version.as_deref(), output_format.as_ref());
583579
},
584-
ResourceSubCommand::Export { resource, input, file, output_format } => {
585-
dsc.find_resources(&[resource.to_string()], progress_format);
580+
ResourceSubCommand::Export { resource, version, input, file, output_format } => {
581+
dsc.find_resources(&[DiscoveryFilter::new(resource.clone(), version.clone())], progress_format);
586582
let parsed_input = get_input(input.as_ref(), file.as_ref(), false);
587-
resource_command::export(&mut dsc, resource, &parsed_input, output_format.as_ref());
583+
resource_command::export(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
588584
},
589-
ResourceSubCommand::Get { resource, input, file: path, all, output_format } => {
590-
dsc.find_resources(&[resource.to_string()], progress_format);
585+
ResourceSubCommand::Get { resource, version, input, file: path, all, output_format } => {
586+
dsc.find_resources(&[DiscoveryFilter::new(resource.clone(), version.clone())], progress_format);
591587
if *all {
592-
resource_command::get_all(&dsc, resource, output_format.as_ref());
588+
resource_command::get_all(&mut dsc, resource, version.as_deref(), output_format.as_ref());
593589
}
594590
else {
595591
if *output_format == Some(GetOutputFormat::JsonArray) {
596592
error!("{}", t!("subcommand.jsonArrayNotSupported"));
597593
exit(EXIT_INVALID_ARGS);
598594
}
599595
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
600-
resource_command::get(&dsc, resource, &parsed_input, output_format.as_ref());
596+
resource_command::get(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
601597
}
602598
},
603-
ResourceSubCommand::Set { resource, input, file: path, output_format } => {
604-
dsc.find_resources(&[resource.to_string()], progress_format);
599+
ResourceSubCommand::Set { resource, version, input, file: path, output_format } => {
600+
dsc.find_resources(&[DiscoveryFilter::new(resource.clone(), version.clone())], progress_format);
605601
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
606-
resource_command::set(&dsc, resource, &parsed_input, output_format.as_ref());
602+
resource_command::set(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
607603
},
608-
ResourceSubCommand::Test { resource, input, file: path, output_format } => {
609-
dsc.find_resources(&[resource.to_string()], progress_format);
604+
ResourceSubCommand::Test { resource, version, input, file: path, output_format } => {
605+
dsc.find_resources(&[DiscoveryFilter::new(resource.clone(), version.clone())], progress_format);
610606
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
611-
resource_command::test(&dsc, resource, &parsed_input, output_format.as_ref());
607+
resource_command::test(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
612608
},
613-
ResourceSubCommand::Delete { resource, input, file: path } => {
614-
dsc.find_resources(&[resource.to_string()], progress_format);
609+
ResourceSubCommand::Delete { resource, version, input, file: path } => {
610+
dsc.find_resources(&[DiscoveryFilter::new(resource.clone(), version.clone())], progress_format);
615611
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
616-
resource_command::delete(&dsc, resource, &parsed_input);
612+
resource_command::delete(&mut dsc, resource, version.as_deref(), &parsed_input);
617613
},
618614
}
619615
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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+
}

0 commit comments

Comments
 (0)