Skip to content

Add support for discovery extensions #760

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions dsc/locales/en-us.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ configAbout = "Apply a configuration document"
parameters = "Parameters to pass to the configuration as JSON or YAML"
parametersFile = "Parameters to pass to the configuration as a JSON or YAML file"
systemRoot = "Specify the operating system root path if not targeting the current running OS"
extensionAbout = "Operations on DSC extensions"
resourceAbout = "Invoke a specific DSC resource"
schemaAbout = "Get the JSON schema for a DSC type"
schemaType = "The type of DSC schema to get"
Expand All @@ -24,6 +25,7 @@ validateAbout = "Validate the current configuration"
exportAbout = "Export the current configuration"
resolveAbout = "Resolve the current configuration"
listAbout = "List or find resources"
listExtensionAbout = "List or find extensions"
adapter = "Adapter filter to limit the resource search"
description = "Description keyword to search for in the resource description"
tags = "Tag to search for in the resource tags"
Expand Down
16 changes: 16 additions & 0 deletions dsc/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ pub enum SubCommand {
#[clap(long, hide = true)]
as_include: bool,
},
#[clap(name = "extension", about = t!("args.extensionAbout").to_string())]
Extension {
#[clap(subcommand)]
subcommand: ExtensionSubCommand,
},
#[clap(name = "resource", about = t!("args.resourceAbout").to_string())]
Resource {
#[clap(subcommand)]
Expand Down Expand Up @@ -144,6 +149,17 @@ pub enum ConfigSubCommand {
}
}

#[derive(Debug, PartialEq, Eq, Subcommand)]
pub enum ExtensionSubCommand {
#[clap(name = "list", about = t!("args.listExtensionAbout").to_string())]
List {
/// Optional filter to apply to the list of extensions
extension_name: Option<String>,
#[clap(short = 'o', long, help = t!("args.outputFormat").to_string())]
output_format: Option<OutputFormat>,
},
}

#[derive(Debug, PartialEq, Eq, Subcommand)]
pub enum ResourceSubCommand {
#[clap(name = "list", about = t!("args.listAbout").to_string())]
Expand Down
3 changes: 3 additions & 0 deletions dsc/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ fn main() {
subcommand::config(&subcommand, &parameters, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format);
}
},
SubCommand::Extension { subcommand } => {
subcommand::extension(&subcommand, progress_format);
},
SubCommand::Resource { subcommand } => {
subcommand::resource(&subcommand, progress_format);
},
Expand Down
217 changes: 147 additions & 70 deletions dsc/src/subcommand.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::args::{ConfigSubCommand, DscType, OutputFormat, ResourceSubCommand};
use crate::args::{ConfigSubCommand, DscType, ExtensionSubCommand, OutputFormat, ResourceSubCommand};
use crate::resolve::{get_contents, Include};
use crate::resource_command::{get_resource, self};
use crate::tablewriter::Table;
Expand All @@ -16,6 +16,8 @@ use dsc_lib::{
config_result::ResourceGetResult,
Configurator,
},
discovery::discovery_trait::DiscoveryKind,
discovery::command_discovery::ManifestResource,
dscerror::DscError,
DscManager,
dscresources::invoke_result::{
Expand All @@ -25,6 +27,7 @@ use dsc_lib::{
},
dscresources::dscresource::{Capability, ImplementedAs, Invoke},
dscresources::resource_manifest::{import_manifest, ResourceManifest},
extensions::dscextension::Capability as ExtensionCapability,
progress::ProgressFormat,
};
use rust_i18n::t;
Expand Down Expand Up @@ -543,6 +546,22 @@ pub fn validate_config(config: &Configuration, progress_format: ProgressFormat)
Ok(())
}

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);
}
};

match subcommand {
ExtensionSubCommand::List{extension_name, output_format} => {
list_extensions(&mut dsc, extension_name.as_ref(), output_format.as_ref(), progress_format);
},
}
}

#[allow(clippy::too_many_lines)]
pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat) {
let mut dsc = match DscManager::new() {
Expand Down Expand Up @@ -592,6 +611,62 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat
}
}

fn list_extensions(dsc: &mut DscManager, extension_name: Option<&String>, format: Option<&OutputFormat>, progress_format: ProgressFormat) {
let mut write_table = false;
let mut table = Table::new(&[
t!("subcommand.tableHeader_type").to_string().as_ref(),
t!("subcommand.tableHeader_version").to_string().as_ref(),
t!("subcommand.tableHeader_capabilities").to_string().as_ref(),
t!("subcommand.tableHeader_description").to_string().as_ref(),
]);
if format.is_none() && io::stdout().is_terminal() {
// write as table if format is not specified and interactive
write_table = true;
}
let mut include_separator = false;
for manifest_resource in dsc.list_available(&DiscoveryKind::Extension, extension_name.unwrap_or(&String::from("*")), "", progress_format) {
if let ManifestResource::Extension(extension) = manifest_resource {
let mut capabilities = "-".to_string();
let capability_types = [
(ExtensionCapability::Discover, "d"),
];

for (i, (capability, letter)) in capability_types.iter().enumerate() {
if extension.capabilities.contains(capability) {
capabilities.replace_range(i..=i, letter);
}
}

if write_table {
table.add_row(vec![
extension.type_name,
extension.version,
capabilities,
extension.description.unwrap_or_default()
]);
}
else {
// convert to json
let json = match serde_json::to_string(&extension) {
Ok(json) => json,
Err(err) => {
error!("JSON: {err}");
exit(EXIT_JSON_ERROR);
}
};
write_object(&json, format, include_separator);
include_separator = true;
// insert newline separating instances if writing to console
if io::stdout().is_terminal() { println!(); }
}
}
}

if write_table {
table.print();
}
}

fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adapter_name: Option<&String>, description: Option<&String>, tags: Option<&Vec<String>>, format: Option<&OutputFormat>, progress_format: ProgressFormat) {
let mut write_table = false;
let mut table = Table::new(&[
Expand All @@ -607,86 +682,88 @@ fn list_resources(dsc: &mut DscManager, resource_name: Option<&String>, adapter_
write_table = true;
}
let mut include_separator = false;
for resource in dsc.list_available_resources(resource_name.unwrap_or(&String::from("*")), adapter_name.unwrap_or(&String::new()), progress_format) {
let mut capabilities = "--------".to_string();
let capability_types = [
(Capability::Get, "g"),
(Capability::Set, "s"),
(Capability::SetHandlesExist, "x"),
(Capability::WhatIf, "w"),
(Capability::Test, "t"),
(Capability::Delete, "d"),
(Capability::Export, "e"),
(Capability::Resolve, "r"),
];

for (i, (capability, letter)) in capability_types.iter().enumerate() {
if resource.capabilities.contains(capability) {
capabilities.replace_range(i..=i, letter);
for manifest_resource in dsc.list_available(&DiscoveryKind::Resource, resource_name.unwrap_or(&String::from("*")), adapter_name.unwrap_or(&String::new()), progress_format) {
if let ManifestResource::Resource(resource) = manifest_resource {
let mut capabilities = "--------".to_string();
let capability_types = [
(Capability::Get, "g"),
(Capability::Set, "s"),
(Capability::SetHandlesExist, "x"),
(Capability::WhatIf, "w"),
(Capability::Test, "t"),
(Capability::Delete, "d"),
(Capability::Export, "e"),
(Capability::Resolve, "r"),
];

for (i, (capability, letter)) in capability_types.iter().enumerate() {
if resource.capabilities.contains(capability) {
capabilities.replace_range(i..=i, letter);
}
}
}

// if description, tags, or write_table is specified, pull resource manifest if it exists
if let Some(ref resource_manifest) = resource.manifest {
let manifest = match import_manifest(resource_manifest.clone()) {
Ok(resource_manifest) => resource_manifest,
Err(err) => {
error!("{} {}: {err}", t!("subcommand.invalidManifest"), resource.type_name);
// if description, tags, or write_table is specified, pull resource manifest if it exists
if let Some(ref resource_manifest) = resource.manifest {
let manifest = match import_manifest(resource_manifest.clone()) {
Ok(resource_manifest) => resource_manifest,
Err(err) => {
error!("{} {}: {err}", t!("subcommand.invalidManifest"), resource.type_name);
continue;
}
};

// if description is specified, skip if resource description does not contain it
if description.is_some() &&
(manifest.description.is_none() | !manifest.description.unwrap_or_default().to_lowercase().contains(&description.unwrap_or(&String::new()).to_lowercase())) {
continue;
}
};

// if description is specified, skip if resource description does not contain it
if description.is_some() &&
(manifest.description.is_none() | !manifest.description.unwrap_or_default().to_lowercase().contains(&description.unwrap_or(&String::new()).to_lowercase())) {
continue;
}

// if tags is specified, skip if resource tags do not contain the tags
if let Some(tags) = tags {
let Some(manifest_tags) = manifest.tags else { continue; };
// if tags is specified, skip if resource tags do not contain the tags
if let Some(tags) = tags {
let Some(manifest_tags) = manifest.tags else { continue; };

let mut found = false;
for tag_to_find in tags {
for tag in &manifest_tags {
if tag.to_lowercase() == tag_to_find.to_lowercase() {
found = true;
break;
let mut found = false;
for tag_to_find in tags {
for tag in &manifest_tags {
if tag.to_lowercase() == tag_to_find.to_lowercase() {
found = true;
break;
}
}
}
if !found { continue; }
}
} else {
// resource does not have a manifest but filtering on description or tags was requested - skip such resource
if description.is_some() || tags.is_some() {
continue;
}
if !found { continue; }
}
} else {
// resource does not have a manifest but filtering on description or tags was requested - skip such resource
if description.is_some() || tags.is_some() {
continue;
}
}

if write_table {
table.add_row(vec![
resource.type_name,
format!("{:?}", resource.kind),
resource.version,
capabilities,
resource.require_adapter.unwrap_or_default(),
resource.description.unwrap_or_default()
]);
}
else {
// convert to json
let json = match serde_json::to_string(&resource) {
Ok(json) => json,
Err(err) => {
error!("JSON: {err}");
exit(EXIT_JSON_ERROR);
}
};
write_object(&json, format, include_separator);
include_separator = true;
// insert newline separating instances if writing to console
if io::stdout().is_terminal() { println!(); }
if write_table {
table.add_row(vec![
resource.type_name,
format!("{:?}", resource.kind),
resource.version,
capabilities,
resource.require_adapter.unwrap_or_default(),
resource.description.unwrap_or_default()
]);
}
else {
// convert to json
let json = match serde_json::to_string(&resource) {
Ok(json) => json,
Err(err) => {
error!("JSON: {err}");
exit(EXIT_JSON_ERROR);
}
};
write_object(&json, format, include_separator);
include_separator = true;
// insert newline separating instances if writing to console
if io::stdout().is_terminal() { println!(); }
}
}
}

Expand Down
Loading
Loading