Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dsc/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dsc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ path-absolutize = { version = "3.1" }
regex = "1.11"
rust-i18n = { version = "3.1" }
schemars = { version = "1.0" }
semver = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = { version = "0.9" }
Expand Down
1 change: 1 addition & 0 deletions dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ getAll = "Get all instances of the resource"
resource = "The name of the resource to invoke"
functionAbout = "Operations on DSC functions"
listFunctionAbout = "List or find functions"
version = "The version of the resource to invoke in semver format"

[main]
ctrlCReceived = "Ctrl-C received"
Expand Down
12 changes: 12 additions & 0 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ pub enum ResourceSubCommand {
all: bool,
#[clap(short, long, help = t!("args.resource").to_string())]
resource: String,
#[clap(short, long, help = t!("args.version").to_string())]
version: Option<String>,
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
input: Option<String>,
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
Expand All @@ -226,6 +228,8 @@ pub enum ResourceSubCommand {
Set {
#[clap(short, long, help = t!("args.resource").to_string())]
resource: String,
#[clap(short, long, help = t!("args.version").to_string())]
version: Option<String>,
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
input: Option<String>,
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
Expand All @@ -237,6 +241,8 @@ pub enum ResourceSubCommand {
Test {
#[clap(short, long, help = t!("args.resource").to_string())]
resource: String,
#[clap(short, long, help = t!("args.version").to_string())]
version: Option<String>,
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
input: Option<String>,
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
Expand All @@ -248,6 +254,8 @@ pub enum ResourceSubCommand {
Delete {
#[clap(short, long, help = t!("args.resource").to_string())]
resource: String,
#[clap(short, long, help = t!("args.version").to_string())]
version: Option<String>,
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
input: Option<String>,
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
Expand All @@ -257,13 +265,17 @@ pub enum ResourceSubCommand {
Schema {
#[clap(short, long, help = t!("args.resource").to_string())]
resource: String,
#[clap(short, long, help = t!("args.version").to_string())]
version: Option<String>,
#[clap(short = 'o', long, help = t!("args.outputFormat").to_string())]
output_format: Option<OutputFormat>,
},
#[clap(name = "export", about = "Retrieve all resource instances", arg_required_else_help = true)]
Export {
#[clap(short, long, help = t!("args.resource").to_string())]
resource: String,
#[clap(short, long, help = t!("args.version").to_string())]
version: Option<String>,
#[clap(short, long, help = t!("args.input").to_string(), conflicts_with = "file")]
input: Option<String>,
#[clap(short = 'f', long, help = t!("args.file").to_string(), conflicts_with = "input")]
Expand Down
46 changes: 23 additions & 23 deletions dsc/src/resource_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ use dsc_lib::{
};
use std::process::exit;

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

Expand Down Expand Up @@ -67,10 +67,10 @@ pub fn get(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&G
}
}

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

Expand Down Expand Up @@ -125,14 +125,14 @@ pub fn get_all(dsc: &DscManager, resource_type: &str, format: Option<&GetOutputF
}
}

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

let Some(resource) = get_resource(dsc, resource_type) else {
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
let Some(resource) = get_resource(dsc, resource_type, version) else {
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
};

Expand Down Expand Up @@ -161,14 +161,14 @@ pub fn set(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&O
}
}

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

let Some(resource) = get_resource(dsc, resource_type) else {
error!("{}", DscError::ResourceNotFound(resource_type.to_string()).to_string());
let Some(resource) = get_resource(dsc, resource_type, version) else {
error!("{}", DscError::ResourceNotFound(resource_type.to_string(), version.unwrap_or("").to_string()).to_string());
exit(EXIT_DSC_RESOURCE_NOT_FOUND);
};

Expand Down Expand Up @@ -197,9 +197,9 @@ pub fn test(dsc: &DscManager, resource_type: &str, input: &str, format: Option<&
}
}

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

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

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

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

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

#[must_use]
pub fn get_resource<'a>(dsc: &'a DscManager, resource: &str) -> Option<&'a DscResource> {
pub fn get_resource<'a>(dsc: &'a mut DscManager, resource: &str, version: Option<&str>) -> Option<&'a DscResource> {
//TODO: add dynamically generated resource to dsc
dsc.find_resource(resource)
dsc.find_resource(resource, version)
}
52 changes: 24 additions & 28 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use dsc_lib::{
config_result::ResourceGetResult,
Configurator,
},
discovery::discovery_trait::DiscoveryKind,
discovery::discovery_trait::{DiscoveryFilter, DiscoveryKind},
discovery::command_discovery::ImportedManifest,
dscerror::DscError,
DscManager,
Expand All @@ -35,6 +35,7 @@ use dsc_lib::{
};
use regex::RegexBuilder;
use rust_i18n::t;
use core::convert::AsRef;
use std::{
collections::HashMap,
io::{self, IsTerminal},
Expand Down Expand Up @@ -488,17 +489,12 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
};

// discover the resources
let mut resource_types = Vec::new();
let mut resource_types = Vec::<DiscoveryFilter>::new();
for resource_block in resources {
let Some(type_name) = resource_block["type"].as_str() else {
return Err(DscError::Validation(t!("subcommand.resourceTypeNotSpecified").to_string()));
};

if resource_types.contains(&type_name.to_lowercase()) {
continue;
}

resource_types.push(type_name.to_lowercase().to_string());
resource_types.push(DiscoveryFilter::new(&type_name.to_lowercase(), resource_block["api_version"].as_str().map(std::string::ToString::to_string)));
}
dsc.find_resources(&resource_types, progress_format);

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

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

Expand Down Expand Up @@ -577,43 +573,43 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat
ResourceSubCommand::List { resource_name, adapter_name, description, tags, output_format } => {
list_resources(&mut dsc, resource_name.as_ref(), adapter_name.as_ref(), description.as_ref(), tags.as_ref(), output_format.as_ref(), progress_format);
},
ResourceSubCommand::Schema { resource , output_format } => {
dsc.find_resources(&[resource.to_string()], progress_format);
resource_command::schema(&dsc, resource, output_format.as_ref());
ResourceSubCommand::Schema { resource , version, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
resource_command::schema(&mut dsc, resource, version.as_deref(), output_format.as_ref());
},
ResourceSubCommand::Export { resource, input, file, output_format } => {
dsc.find_resources(&[resource.to_string()], progress_format);
ResourceSubCommand::Export { resource, version, input, file, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), file.as_ref(), false);
resource_command::export(&mut dsc, resource, &parsed_input, output_format.as_ref());
resource_command::export(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
},
ResourceSubCommand::Get { resource, input, file: path, all, output_format } => {
dsc.find_resources(&[resource.to_string()], progress_format);
ResourceSubCommand::Get { resource, version, input, file: path, all, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
if *all {
resource_command::get_all(&dsc, resource, output_format.as_ref());
resource_command::get_all(&mut dsc, resource, version.as_deref(), output_format.as_ref());
}
else {
if *output_format == Some(GetOutputFormat::JsonArray) {
error!("{}", t!("subcommand.jsonArrayNotSupported"));
exit(EXIT_INVALID_ARGS);
}
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
resource_command::get(&dsc, resource, &parsed_input, output_format.as_ref());
resource_command::get(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
}
},
ResourceSubCommand::Set { resource, input, file: path, output_format } => {
dsc.find_resources(&[resource.to_string()], progress_format);
ResourceSubCommand::Set { resource, version, input, file: path, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
resource_command::set(&dsc, resource, &parsed_input, output_format.as_ref());
resource_command::set(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
},
ResourceSubCommand::Test { resource, input, file: path, output_format } => {
dsc.find_resources(&[resource.to_string()], progress_format);
ResourceSubCommand::Test { resource, version, input, file: path, output_format } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
resource_command::test(&dsc, resource, &parsed_input, output_format.as_ref());
resource_command::test(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref());
},
ResourceSubCommand::Delete { resource, input, file: path } => {
dsc.find_resources(&[resource.to_string()], progress_format);
ResourceSubCommand::Delete { resource, version, input, file: path } => {
dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format);
let parsed_input = get_input(input.as_ref(), path.as_ref(), false);
resource_command::delete(&dsc, resource, &parsed_input);
resource_command::delete(&mut dsc, resource, version.as_deref(), &parsed_input);
},
}
}
Expand Down
80 changes: 80 additions & 0 deletions dsc/tests/dsc_config_version.tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

Describe 'Tests for resource versioning' {
It "Should return the correct version '<version>' for operation '<operation>'" -TestCases @(
@{ version = '1.1.2'; operation = 'get'; property = 'actualState' }
@{ version = '1.1.0'; operation = 'get'; property = 'actualState' }
@{ version = '2.0.0'; operation = 'get'; property = 'actualState' }
@{ version = '1.1.2'; operation = 'set'; property = 'afterState' }
@{ version = '1.1.0'; operation = 'set'; property = 'afterState' }
@{ version = '2.0.0'; operation = 'set'; property = 'afterState' }
@{ version = '1.1.2'; operation = 'test'; property = 'actualState' }
@{ version = '1.1.0'; operation = 'test'; property = 'actualState' }
@{ version = '2.0.0'; operation = 'test'; property = 'actualState' }
) {
param($version, $operation, $property)
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Test Version
type: Test/Version
apiVersion: $version
properties:
version: $version
"@
$out = dsc -l trace config $operation -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
$out.results[0].result.$property.version | Should -BeExactly $version
}

It "Version requirements '<req>' should return correct version" -TestCases @(
@{ req = '>=1.0.0' ; expected = '2.0.0' }
@{ req = '<=1.1.0' ; expected = '1.1.0' }
@{ req = '<1.3' ; expected = '1.1.3' }
@{ req = '>1,<=2.0.0' ; expected = '2.0.0' }
@{ req = '>1.0.0,<2.0.0' ; expected = '1.1.3' }
@{ req = '1'; expected = '1.1.3' }
@{ req = '1.1' ; expected = '1.1.3' }
@{ req = '^1.0' ; expected = '1.1.3' }
@{ req = '~1.1' ; expected = '1.1.3' }
@{ req = '*' ; expected = '2.0.0' }
@{ req = '1.*' ; expected = '1.1.3' }
@{ req = '2.1.0-preview.2' ; expected = '2.1.0-preview.2' }
) {
param($req, $expected)
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Test Version
type: Test/Version
apiVersion: '$req'
properties:
version: $expected
"@
$out = dsc -l trace config test -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
$out.results[0].result.actualState.version | Should -BeExactly $expected
}

It 'Multiple versions should be handled correctly' {
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: Test Version 1
type: Test/Version
apiVersion: '1.1.2'
- name: Test Version 2
type: Test/Version
apiVersion: '1.1.0'
- name: Test Version 3
type: Test/Version
apiVersion: '2'
"@
$out = dsc -l trace config get -i $config_yaml 2> $TestDrive/error.log | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0 -Because (Get-Content $TestDrive/error.log -Raw)
$out.results[0].result.actualState.version | Should -BeExactly '1.1.2'
$out.results[1].result.actualState.version | Should -BeExactly '1.1.0'
$out.results[2].result.actualState.version | Should -BeExactly '2.0.0'
}
}
6 changes: 6 additions & 0 deletions dsc/tests/dsc_resource_get.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,10 @@ Describe 'resource get tests' {
$out.bitness | Should -BeIn @('32', '64')
$out.architecture | Should -BeIn @('x86', 'x86_64', 'arm64')
}

It 'version works' {
$out = dsc resource get -r Test/Version --version 1.1.2 | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.actualState.version | Should -BeExactly '1.1.2'
}
}
Loading
Loading