Skip to content

Enable passing Bicep files directly to dsc #997

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 24, 2025
Merged
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
7 changes: 5 additions & 2 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ if ($GetPackageVersion) {
$filesForWindowsPackage = @(
'appx.dsc.extension.json',
'appx-discover.ps1',
'bicep.dsc.extension.json',
'dsc.exe',
'dsc_default.settings.json',
'dsc.settings.json',
Expand Down Expand Up @@ -75,6 +76,7 @@ $filesForWindowsPackage = @(
)

$filesForLinuxPackage = @(
'bicep.dsc.extension.json',
'dsc',
'dsc_default.settings.json',
'dsc.settings.json'
Expand All @@ -99,6 +101,7 @@ $filesForLinuxPackage = @(
)

$filesForMacPackage = @(
'bicep.dsc.extension.json',
'dsc',
'dsc_default.settings.json',
'dsc.settings.json'
Expand Down Expand Up @@ -305,6 +308,7 @@ if (!$SkipBuild) {
"dsc_lib",
"dsc",
"dscecho",
"extensions/bicep",
"osinfo",
"powershell-adapter",
'resources/PSScript',
Expand All @@ -313,8 +317,7 @@ if (!$SkipBuild) {
"sshdconfig",
"tools/dsctest",
"tools/test_group_resource",
"y2j",
"."
"y2j"
)
$pedantic_unclean_projects = @()
$clippy_unclean_projects = @("tree-sitter-dscexpression", "tree-sitter-ssh-server-config")
Expand Down
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.

5 changes: 5 additions & 0 deletions dsc/examples/bicepconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"experimentalFeaturesEnabled": {
"desiredStateConfiguration": true
}
}
11 changes: 11 additions & 0 deletions dsc/examples/hello_world.dsc.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This is a very simple example Bicep file for testing

targetScope = 'desiredStateConfiguration'

// use workaround where Bicep currently requires version in date format
resource echo 'Microsoft.DSC.Debug/Echo@2025-01-01' = {
name: 'exampleEcho'
properties: {
output: 'Hello, world!'
}
}
18 changes: 3 additions & 15 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
let schema = serde_json::to_value(get_schema(SchemaType::Configuration))?;
let config_value = serde_json::to_value(config)?;
validate_json("Configuration", &schema, &config_value)?;
let mut dsc = DscManager::new()?;
let mut dsc = DscManager::new();

// then validate each resource
let Some(resources) = config_value["resources"].as_array() else {
Expand Down Expand Up @@ -551,13 +551,7 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
}

pub fn extension(subcommand: &ExtensionSubCommand, progress_format: ProgressFormat) {
let mut dsc = match DscManager::new() {
Ok(dsc) => dsc,
Err(err) => {
error!("Error: {err}");
exit(EXIT_DSC_ERROR);
}
};
let mut dsc = DscManager::new();

match subcommand {
ExtensionSubCommand::List{extension_name, output_format} => {
Expand All @@ -577,13 +571,7 @@ pub fn function(subcommand: &FunctionSubCommand) {

#[allow(clippy::too_many_lines)]
pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat) {
let mut dsc = match DscManager::new() {
Ok(dsc) => dsc,
Err(err) => {
error!("Error: {err}");
exit(EXIT_DSC_ERROR);
}
};
let mut dsc = DscManager::new();

match subcommand {
ResourceSubCommand::List { resource_name, adapter_name, description, tags, output_format } => {
Expand Down
33 changes: 24 additions & 9 deletions dsc/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@

use crate::args::{SchemaType, OutputFormat, TraceFormat};
use crate::resolve::Include;
use dsc_lib::configure::config_result::ResourceTestResult;
use dsc_lib::extensions::discover::DiscoverResult;
use dsc_lib::extensions::extension_manifest::ExtensionManifest;
use dsc_lib::{
configure::{
config_doc::{
Expand All @@ -16,22 +13,33 @@ use dsc_lib::{
config_result::{
ConfigurationGetResult,
ConfigurationSetResult,
ConfigurationTestResult
}
ConfigurationTestResult,
ResourceTestResult,
},
},
discovery::Discovery,
dscerror::DscError,
dscresources::{
command_resource::TraceLevel,
dscresource::DscResource, invoke_result::{
dscresource::DscResource,
invoke_result::{
GetResult,
SetResult,
TestResult,
ResolveResult,
}, resource_manifest::ResourceManifest
},
resource_manifest::ResourceManifest
},
extensions::{
discover::DiscoverResult,
dscextension::Capability,
extension_manifest::ExtensionManifest,
},
functions::FunctionDefinition,
util::parse_input_to_json,
util::get_setting,
util::{
get_setting,
parse_input_to_json,
},
};
use jsonschema::Validator;
use path_absolutize::Absolutize;
Expand Down Expand Up @@ -487,6 +495,13 @@ pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_
}
}
} else {
// see if an extension should handle this file
let mut discovery = Discovery::new();
for extension in discovery.get_extensions(&Capability::Import) {
if let Ok(content) = extension.import(path) {
return content;
}
}
match std::fs::read_to_string(path) {
Ok(input) => {
// check if it contains UTF-8 BOM and remove it
Expand Down
14 changes: 9 additions & 5 deletions dsc/tests/dsc_extension_discover.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ Describe 'Discover extension tests' {
It 'Discover extensions' {
$out = dsc extension list | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.Count | Should -Be 1
$out.type | Should -BeExactly 'Test/Discover'
$out.version | Should -BeExactly '0.1.0'
$out.capabilities | Should -BeExactly @('discover')
$out.manifest | Should -Not -BeNullOrEmpty
$out.Count | Should -Be 2 -Because ($out | Out-String)
$out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep'
$out[0].version | Should -Be '0.1.0'
$out[0].capabilities | Should -BeExactly @('import')
$out[0].manifest | Should -Not -BeNullOrEmpty
$out[1].type | Should -BeExactly 'Test/Discover'
$out[1].version | Should -BeExactly '0.1.0'
$out[1].capabilities | Should -BeExactly @('discover')
$out[1].manifest | Should -Not -BeNullOrEmpty
}

It 'Filtering works for extension discovered resources' {
Expand Down
19 changes: 19 additions & 0 deletions dsc_lib/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_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ indicatif = "0.18"
jsonschema = { version = "0.30", default-features = false }
linked-hash-map = "0.5"
num-traits = "0.2"
path-absolutize = { version = "3.1" }
regex = "1.11"
rt-format = "0.3"
rust-i18n = { version = "3.1" }
Expand Down
5 changes: 5 additions & 0 deletions dsc_lib/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ extensionResourceFound = "Extension found resource '%{resource}'"
callingExtension = "Calling extension '%{extension}' to discover resources"
extensionFoundResources = "Extension '%{extension}' found %{count} resources"
invalidManifestVersion = "Manifest '%{path}' has invalid version: %{err}"
importExtensionsEmpty = "Import extension '%{extension}' has no import extensions defined"

[dscresources.commandResource]
invokeGet = "Invoking get for '%{resource}'"
Expand Down Expand Up @@ -183,6 +184,9 @@ secretExtensionReturnedInvalidJson = "Extension '%{extension}' returned invalid
extensionReturnedSecret = "Extension '%{extension}' returned secret"
extensionReturnedNoSecret = "Extension '%{extension}' did not return a secret"
secretNoResults = "Extension '%{extension}' returned no output"
importingFile = "Importing file '%{file}' with extension '%{extension}'"
importNotSupported = "Import is not supported by extension '%{extension}' for file '%{file}'"
importNoResults = "Extension '%{extension}' returned no results for import"

[extensions.extension_manifest]
extensionManifestSchemaTitle = "Extension manifest schema URI"
Expand Down Expand Up @@ -453,3 +457,4 @@ foundSetting = "Found setting '%{name}' in %{path}"
notFoundSetting = "Setting '%{name}' not found in %{path}"
failedToGetExePath = "Can't get 'dsc' executable path"
settingNotFound = "Setting '%{name}' not found"
failedToAbsolutizePath = "Failed to absolutize path '%{path}'"
2 changes: 1 addition & 1 deletion dsc_lib/src/configure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ 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> {
let discovery = Discovery::new()?;
let discovery = Discovery::new();
let mut config = Configurator {
json: json.to_owned(),
config: Configuration::new(),
Expand Down
13 changes: 13 additions & 0 deletions dsc_lib/src/discovery/command_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -726,12 +726,25 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result<
verify_executable(&manifest.r#type, "secret", &secret.executable);
capabilities.push(dscextension::Capability::Secret);
}
let import_extensions = if let Some(import) = &manifest.import {
verify_executable(&manifest.r#type, "import", &import.executable);
capabilities.push(dscextension::Capability::Import);
if import.file_extensions.is_empty() {
warn!("{}", t!("discovery.commandDiscovery.importExtensionsEmpty", extension = manifest.r#type));
None
} else {
Some(import.file_extensions.clone())
}
} else {
None
};

let extension = DscExtension {
type_name: manifest.r#type.clone(),
description: manifest.description.clone(),
version: manifest.version.clone(),
capabilities,
import_file_extensions: import_extensions,
path: path.to_str().unwrap().to_string(),
directory: path.parent().unwrap().to_str().unwrap().to_string(),
manifest: serde_json::to_value(manifest)?,
Expand Down
28 changes: 22 additions & 6 deletions dsc_lib/src/discovery/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ pub mod command_discovery;
pub mod discovery_trait;

use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery};
use crate::extensions::dscextension::DscExtension;
use crate::{dscresources::dscresource::DscResource, dscerror::DscError, progress::ProgressFormat};
use crate::extensions::dscextension::{Capability, DscExtension};
use crate::{dscresources::dscresource::DscResource, progress::ProgressFormat};
use core::result::Result::Ok;
use std::collections::BTreeMap;
use command_discovery::{CommandDiscovery, ImportedManifest};
use tracing::error;
Expand All @@ -24,11 +25,12 @@ impl Discovery {
///
/// This function will return an error if the underlying instance creation fails.
///
pub fn new() -> Result<Self, DscError> {
Ok(Self {
#[must_use]
pub fn new() -> Self {
Self {
resources: BTreeMap::new(),
extensions: BTreeMap::new(),
})
}
}

/// List operation for getting available resources based on the filters.
Expand Down Expand Up @@ -64,11 +66,25 @@ impl Discovery {
resources.push(resource.clone());
}
};

if let Ok(extensions) = discovery_type.get_extensions() {
self.extensions.extend(extensions);
}
}

resources
}

pub fn get_extensions(&mut self, capability: &Capability) -> Vec<DscExtension> {
if self.extensions.is_empty() {
self.list_available(&DiscoveryKind::Extension, "*", "", ProgressFormat::None);
}
self.extensions.values()
.filter(|ext| ext.capabilities.contains(capability))
.cloned()
.collect()
}

#[must_use]
pub fn find_resource(&self, type_name: &str) -> Option<&DscResource> {
self.resources.get(&type_name.to_lowercase())
Expand Down Expand Up @@ -108,7 +124,7 @@ impl Discovery {

impl Default for Discovery {
fn default() -> Self {
Self::new().unwrap()
Self::new()
}
}

Expand Down
Loading
Loading