Skip to content
Open
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/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,4 @@ dscConfigRootAlreadySet = "The current value of DSC_CONFIG_ROOT env var will be
settingDscConfigRoot = "Setting DSC_CONFIG_ROOT env var as"
removingUtf8Bom = "Removing UTF-8 BOM from input"
parametersNotObject = "Parameters must be an object"
invalidDscVersion = "Unable to parse the DSC version from the cargo manifest: %{error}"
5 changes: 2 additions & 3 deletions dsc/src/server/invoke_dsc_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use crate::server::mcp_server::McpServer;
use crate::util::current_dsc_version;
use dsc_lib::{
configure::{
config_doc::{Configuration, ExecutionKind},
Expand Down Expand Up @@ -126,13 +127,11 @@ impl McpServer {
}
};

let mut configurator = match Configurator::new(&config_json, ProgressFormat::None) {
let mut configurator = match Configurator::new_with_dsc_version(&config_json, ProgressFormat::None, current_dsc_version()) {
Ok(configurator) => configurator,
Err(e) => return Err(McpError::internal_error(e.to_string(), None)),
};

configurator.context.dsc_version = Some(env!("CARGO_PKG_VERSION").to_string());

if what_if.unwrap_or(false) {
if !matches!(operation, ConfigOperation::Set) {
return Err(McpError::invalid_params(
Expand Down
6 changes: 2 additions & 4 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::args::{ConfigSubCommand, SchemaType, ExtensionSubCommand, FunctionSub
use crate::resolve::{get_contents, Include};
use crate::resource_command::{get_resource, self};
use crate::tablewriter::Table;
use crate::util::{get_input, get_schema, in_desired_state, set_dscconfigroot, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR};
use crate::util::{current_dsc_version, get_input, get_schema, in_desired_state, set_dscconfigroot, write_object, DSC_CONFIG_ROOT, EXIT_DSC_ASSERTION_FAILED, EXIT_DSC_ERROR, EXIT_INVALID_ARGS, EXIT_INVALID_INPUT, EXIT_JSON_ERROR};
use dsc_lib::types::{FullyQualifiedTypeName, ResourceVersionReq, TypeNameFilter};
use dsc_lib::{
configure::{
Expand Down Expand Up @@ -310,16 +310,14 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option<String>, mounte
}
};

let mut configurator = match Configurator::new(&json_string, progress_format) {
let mut configurator = match Configurator::new_with_dsc_version(&json_string, progress_format, current_dsc_version()) {
Ok(configurator) => configurator,
Err(err) => {
error!("Error: {err}");
return Err(ExitCode::from(EXIT_DSC_ERROR));
}
};

configurator.context.dsc_version = Some(env!("CARGO_PKG_VERSION").to_string());

if let ConfigSubCommand::Set { what_if , .. } = subcommand && *what_if {
configurator.context.execution_type = ExecutionKind::WhatIf;
}
Expand Down
18 changes: 18 additions & 0 deletions dsc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use dsc_lib::{
},
functions::FunctionDefinition,
schemas::dsc_repo::{DscRepoSchema, RecognizedSchemaVersion, SchemaForm, SchemaUriPrefix},
types::SemanticVersion,
util::{
get_setting,
parse_input_to_json,
Expand Down Expand Up @@ -101,6 +102,23 @@ impl Default for TracingSetting {
}
}

/// Returns the current version of DSC as a semantic version.
///
/// # Returns
///
/// * `SemanticVersion` - The version of DSC as defined in the cargo manifest.
///
/// # Panics
///
/// If the version defined in the cargo manifest isn't a valid semantic version, this function
/// panics and reports the parse error.
pub(crate) fn current_dsc_version() -> SemanticVersion {
match SemanticVersion::parse(env!("CARGO_PKG_VERSION")) {
Ok(version) => version,
Err(err) => panic!("{}", t!("util.invalidDscVersion", error = err)),
}
}

/// Get string representation of JSON value.
///
/// # Arguments
Expand Down
78 changes: 77 additions & 1 deletion dsc/tests/dsc_version.tests.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

BeforeDiscovery {
$dscVersion = (dsc --version).Split(" ")[1] -as [System.Management.Automation.SemanticVersion]
$isPrerelease = -not [string]::IsNullOrEmpty($dscVersion.PreReleaseLabel)
}

Describe 'tests for metadata versioning' {
It 'returns the correct dsc semantic version in metadata' {
$config_yaml = @"
Expand Down Expand Up @@ -36,10 +41,38 @@ Describe 'tests for metadata versioning' {
}

It 'returns no error if DSC version satisfies configuration requirement' {
$dscVersion = (dsc --version).Split(" ")[1] -as [System.Management.Automation.SemanticVersion]
# A prerelease build only satisfies a requirement that defines a prerelease segment for the
# same release, so pin a prerelease build to its release cycle, like '^3.4.0-preview'.
$versionReq = if ($dscVersion.PreReleaseLabel) {
'^{0}.{1}.{2}-{3}' -f $dscVersion.Major, $dscVersion.Minor, $dscVersion.Patch, $dscVersion.PreReleaseLabel.Split('.')[0]
} else {
'>=3.1'
}
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
directives:
version: '$versionReq'
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: 'Hello, World!'
"@
$out = $config_yaml | dsc config get -f - 2>$testdrive/error.log
$errorLog = Get-Content -Path $testdrive/error.log -Raw
$errorLog | Should -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
$result = $out | ConvertFrom-Json
$result.results[0].result.actualState.output | Should -BeExactly 'Hello, World!' -Because $out
}

It 'validates the version directive against the running DSC version' {
$dscVersion = (dsc --version).Split(" ")[1]
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
directives:
version: '>=3.1'
version: '=$dscVersion'
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
Expand All @@ -53,4 +86,47 @@ Describe 'tests for metadata versioning' {
$result = $out | ConvertFrom-Json
$result.results[0].result.actualState.output | Should -BeExactly 'Hello, World!' -Because $out
}

It 'reports the running DSC version when the requirement is not satisfied' {
$dscVersion = (dsc --version).Split(" ")[1]
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
directives:
version: '<3.2.0'
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: 'Hello, World!'
"@
$null = $config_yaml | dsc config get -f - 2>$testdrive/error.log
$errorLog = Get-Content -Path $testdrive/error.log -Raw
$errorLog | Should -BeLike "*Validation*Configuration requires DSC version '<3.2.0', but the current version is '$dscVersion'*"
$LASTEXITCODE | Should -Be 2
}

It 'requires a prerelease segment to match a prerelease DSC version for requirement: <requirement>' -Skip:(-not $isPrerelease) -TestCases @(
@{ requirement = '={0}.{1}.{2}'; satisfied = $false }
@{ requirement = '>={0}.{1}.{2}'; satisfied = $false }
@{ requirement = '<{0}.{1}.{2}'; satisfied = $false }
@{ requirement = '^{0}.{1}.{2}-{3}'; satisfied = $true }
@{ requirement = '>={0}.{1}.{2}-{4}'; satisfied = $true }
) {
param($requirement, $satisfied)

$dscVersion = (dsc --version).Split(" ")[1] -as [System.Management.Automation.SemanticVersion]
$versionReq = $requirement -f $dscVersion.Major, $dscVersion.Minor, $dscVersion.Patch, $dscVersion.PreReleaseLabel.Split('.')[0], $dscVersion.PreReleaseLabel
$config_yaml = @"
`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
directives:
version: '$versionReq'
resources:
- name: Echo
type: Microsoft.DSC.Debug/Echo
properties:
output: 'Hello, World!'
"@
$null = $config_yaml | dsc config get -f - 2>$testdrive/error.log
$LASTEXITCODE | Should -Be ($satisfied ? 0 : 2) -Because (Get-Content -Path $testdrive/error.log -Raw)
}
}
26 changes: 24 additions & 2 deletions lib/dsc-lib/src/configure/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use chrono::{DateTime, Local};
use crate::{configure::config_doc::{ExecutionKind, Operation, UserFunctionDefinition}, extensions::dscextension::DscExtension};
use crate::types::SemanticVersion;
use dsc_lib_security_context::{get_security_context, SecurityContext};
use serde_json::{Map, Value};
use std::{collections::HashMap, path::PathBuf};
Expand All @@ -23,7 +24,12 @@ pub enum ProcessMode {
pub struct Context {
pub copy: HashMap<String, i64>,
pub copy_current_loop_name: String,
pub dsc_version: Option<String>,
/// The semantic version of DSC that is processing the configuration.
///
/// Defaults to the version of the `dsc-lib` crate. A host that knows its own version, like
/// the `dsc` CLI, sets it with
/// [`Configurator::new_with_dsc_version`](crate::configure::Configurator::new_with_dsc_version).
pub dsc_version: SemanticVersion,
pub execution_type: ExecutionKind,
pub extensions: Vec<DscExtension>,
pub lambda_raw_args: std::cell::RefCell<Option<Vec<crate::parser::functions::FunctionArg>>>,
Expand Down Expand Up @@ -52,7 +58,7 @@ impl Context {
Self {
copy: HashMap::new(),
copy_current_loop_name: String::new(),
dsc_version: None,
dsc_version: dsc_lib_version(),
execution_type: ExecutionKind::Actual,
extensions: Vec::new(),
lambda_raw_args: std::cell::RefCell::new(None),
Expand Down Expand Up @@ -86,6 +92,22 @@ impl Default for Context {
}
}

/// Returns the version of the `dsc-lib` crate as a semantic version.
///
/// This is the default DSC version for a [`Context`] when the host doesn't provide its own.
///
/// # Panics
///
/// Panics if the version in the cargo manifest isn't a valid semantic version. Cargo rejects
/// such a manifest at build time, so this can't happen for a built crate.
fn dsc_lib_version() -> SemanticVersion {
let manifest_version = env!("CARGO_PKG_VERSION");
match SemanticVersion::parse(manifest_version) {
Ok(version) => version,
Err(err) => panic!("unable to parse '{manifest_version}' as a semantic version: {err}"),
}
}

#[cfg(target_os = "windows")]
fn get_default_os_system_root() -> PathBuf {
// use SYSTEMDRIVE env var to get the default target path, append trailing separator
Expand Down
60 changes: 47 additions & 13 deletions lib/dsc-lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,11 +499,52 @@ impl Configurator {
///
/// This function will return an error if the configuration is invalid or the underlying discovery fails.
pub fn new(json: &str, progress_format: ProgressFormat) -> Result<Configurator, DscError> {
Self::new_with_context(json, progress_format, Context::new())
}

/// Create a new `Configurator` instance for a specific version of DSC.
///
/// The version is used to validate the `version` directive of the configuration and is
/// reported in the result metadata. Use this constructor when the host, like the `dsc` CLI,
/// knows its own version. [`Configurator::new`] uses the version of the `dsc-lib` crate instead.
///
/// # Arguments
///
/// * `json` - The configuration to use in JSON.
/// * `progress_format` - The format to report progress in.
/// * `dsc_version` - The semantic version of DSC that is processing the configuration.
///
/// # Errors
///
/// This function will return an error if the configuration is invalid or the underlying discovery fails.
pub fn new_with_dsc_version(json: &str, progress_format: ProgressFormat, dsc_version: SemanticVersion) -> Result<Configurator, DscError> {
let mut context = Context::new();
context.dsc_version = dsc_version;
Self::new_with_context(json, progress_format, context)
}

/// Create a new `Configurator` instance with a prepared context.
///
/// The public constructors prepare the context and delegate to this function. It validates
/// the configuration against the context, which must already define the values that
/// validation depends on, like the DSC version, and then registers the discovered extensions
/// on the context.
///
/// # Arguments
///
/// * `json` - The configuration to use in JSON.
/// * `progress_format` - The format to report progress in.
/// * `context` - The context to process the configuration with.
///
/// # Errors
///
/// This function will return an error if the configuration is invalid or the underlying discovery fails.
fn new_with_context(json: &str, progress_format: ProgressFormat, context: Context) -> Result<Configurator, DscError> {
Comment thread
Gijsreyn marked this conversation as resolved.
let discovery = Discovery::new();
let mut config = Configurator {
json: json.to_owned(),
config: Configuration::new(),
context: Context::new(),
context,
discovery: discovery.clone(),
statement_parser: Statement::new()?,
progress_format,
Expand Down Expand Up @@ -1250,11 +1291,6 @@ impl Configurator {

fn get_result_metadata(&self, operation: Operation) -> Metadata {
let end_datetime = chrono::Local::now();
let version = self
.context
.dsc_version
.clone()
.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string());
Metadata {
microsoft: Some(
MicrosoftDscMetadata {
Expand All @@ -1265,7 +1301,7 @@ impl Configurator {
restart_required: self.context.restart_required.clone(),
security_context: Some(self.context.security_context.clone()),
start_datetime: Some(self.context.start_datetime.to_rfc3339()),
version: Some(version),
version: Some(self.context.dsc_version.to_string()),
copy_loops: None,
}
),
Expand All @@ -1278,7 +1314,7 @@ impl Configurator {
execution_information.duration = Some(end_datetime.signed_duration_since(self.context.start_datetime).to_string());
execution_information.end_datetime = Some(end_datetime.to_rfc3339());
execution_information.start_datetime = Some(self.context.start_datetime.to_rfc3339());
execution_information.version = self.context.dsc_version.clone();
execution_information.version = Some(self.context.dsc_version.to_string());
execution_information.execution_type = Some(self.context.execution_type.clone());
execution_information.operation = Some(operation);
execution_information.restart_required = self.context.restart_required.clone();
Expand All @@ -1295,11 +1331,9 @@ impl Configurator {
check_security_context(config.metadata.as_ref(), config_security_context.as_ref())?;

if let Some(directives) = &config.directives
&& let Some(version_req) = &directives.version {
let dsc_version = SemanticVersion::parse(env!("CARGO_PKG_VERSION"))?;
if !version_req.matches(&dsc_version) {
return Err(DscError::Validation(t!("configure.mod.versionNotSatisfied", required_version = version_req, current_version = env!("CARGO_PKG_VERSION")).to_string()));
}
&& let Some(version_req) = &directives.version
&& !version_req.matches(&self.context.dsc_version) {
return Err(DscError::Validation(t!("configure.mod.versionNotSatisfied", required_version = version_req, current_version = self.context.dsc_version).to_string()));
}

let mut resource_discovery_mode = ResourceDiscoveryMode::PreDeployment;
Expand Down
Loading