Skip to content
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
88 changes: 87 additions & 1 deletion dsc_lib/src/extensions/discover.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::dscresources::resource_manifest::ArgKind;
use crate::{
discovery::command_discovery::{
load_manifest, ImportedManifest
},
dscerror::DscError,
dscresources::{
command_resource::{
invoke_command, process_args
},
dscresource::DscResource,
resource_manifest::ArgKind,
},
extensions::{
dscextension::{
Capability,
DscExtension,
},
extension_manifest::ExtensionManifest,
},
};
use rust_i18n::t;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tracing::{info, trace};

#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct DiscoverMethod {
Expand All @@ -19,3 +41,67 @@ pub struct DiscoverResult {
#[serde(rename = "manifestPath")]
pub manifest_path: String,
}

impl DscExtension {
/// Perform discovery of resources using the extension.
///
/// # Returns
///
/// A result containing a vector of discovered resources or an error.
///
/// # Errors
///
/// This function will return an error if the discovery fails.
pub fn discover(&self) -> Result<Vec<DscResource>, DscError> {
let mut resources: Vec<DscResource> = Vec::new();

if self.capabilities.contains(&Capability::Discover) {
let extension = match serde_json::from_value::<ExtensionManifest>(self.manifest.clone()) {
Ok(manifest) => manifest,
Err(err) => {
return Err(DscError::Manifest(self.type_name.clone(), err));
}
};
let Some(discover) = extension.discover else {
return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Discover.to_string()));
};
let args = process_args(discover.args.as_ref(), "");
let (_exit_code, stdout, _stderr) = invoke_command(
&discover.executable,
args,
None,
Some(self.directory.as_str()),
None,
extension.exit_codes.as_ref(),
)?;
if stdout.is_empty() {
info!("{}", t!("extensions.dscextension.discoverNoResults", extension = self.type_name));
} else {
for line in stdout.lines() {
trace!("{}", t!("extensions.dscextension.extensionReturned", extension = self.type_name, line = line));
let discover_result: DiscoverResult = match serde_json::from_str(line) {
Ok(result) => result,
Err(err) => {
return Err(DscError::Json(err));
}
};
if !Path::new(&discover_result.manifest_path).is_absolute() {
return Err(DscError::Extension(t!("extensions.dscextension.discoverNotAbsolutePath", extension = self.type_name.clone(), path = discover_result.manifest_path.clone()).to_string()));
}
let manifest_path = Path::new(&discover_result.manifest_path);
// Currently we don't support extensions discovering other extensions
if let ImportedManifest::Resource(resource) = load_manifest(manifest_path)? {
resources.push(resource);
}
}
}

Ok(resources)
} else {
Err(DscError::UnsupportedCapability(
self.type_name.clone(),
Capability::Discover.to_string()
))
}
}
}
255 changes: 1 addition & 254 deletions dsc_lib/src/extensions/dscextension.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use rust_i18n::t;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use schemars::JsonSchema;
use std::{fmt::Display, path::Path};
use tracing::{debug, info, trace};

use crate::{
discovery::command_discovery::{
load_manifest, ImportedManifest
},
dscerror::DscError,
dscresources::{
command_resource::{
invoke_command,
process_args
},
dscresource::DscResource
},
extensions::import::ImportArgKind
};

use super::{discover::DiscoverResult, extension_manifest::ExtensionManifest, secret::SecretArgKind};
use std::fmt::Display;

#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -87,243 +67,10 @@ impl DscExtension {
manifest: Value::Null,
}
}

/// Perform discovery of resources using the extension.
///
/// # Returns
///
/// A result containing a vector of discovered resources or an error.
///
/// # Errors
///
/// This function will return an error if the discovery fails.
pub fn discover(&self) -> Result<Vec<DscResource>, DscError> {
let mut resources: Vec<DscResource> = Vec::new();

if self.capabilities.contains(&Capability::Discover) {
let extension = match serde_json::from_value::<ExtensionManifest>(self.manifest.clone()) {
Ok(manifest) => manifest,
Err(err) => {
return Err(DscError::Manifest(self.type_name.clone(), err));
}
};
let Some(discover) = extension.discover else {
return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Discover.to_string()));
};
let args = process_args(discover.args.as_ref(), "");
let (_exit_code, stdout, _stderr) = invoke_command(
&discover.executable,
args,
None,
Some(self.directory.as_str()),
None,
extension.exit_codes.as_ref(),
)?;
if stdout.is_empty() {
info!("{}", t!("extensions.dscextension.discoverNoResults", extension = self.type_name));
} else {
for line in stdout.lines() {
trace!("{}", t!("extensions.dscextension.extensionReturned", extension = self.type_name, line = line));
let discover_result: DiscoverResult = match serde_json::from_str(line) {
Ok(result) => result,
Err(err) => {
return Err(DscError::Json(err));
}
};
if !Path::new(&discover_result.manifest_path).is_absolute() {
return Err(DscError::Extension(t!("extensions.dscextension.discoverNotAbsolutePath", extension = self.type_name.clone(), path = discover_result.manifest_path.clone()).to_string()));
}
let manifest_path = Path::new(&discover_result.manifest_path);
// Currently we don't support extensions discovering other extensions
if let ImportedManifest::Resource(resource) = load_manifest(manifest_path)? {
resources.push(resource);
}
}
}

Ok(resources)
} else {
Err(DscError::UnsupportedCapability(
self.type_name.clone(),
Capability::Discover.to_string()
))
}
}

/// Import a file based on the extension.
///
/// # Arguments
///
/// * `file` - The file to import.
///
/// # Returns
///
/// A result containing the imported file content or an error.
///
/// # Errors
///
/// This function will return an error if the import fails or if the extension does not support the import capability.
pub fn import(&self, file: &str) -> Result<String, DscError> {
if self.capabilities.contains(&Capability::Import) {
let file_path = Path::new(file);
let file_extension = file_path.extension().and_then(|s| s.to_str()).unwrap_or_default().to_string();
if self.import_file_extensions.as_ref().is_some_and(|exts| exts.contains(&file_extension)) {
debug!("{}", t!("extensions.dscextension.importingFile", file = file, extension = self.type_name));
} else {
debug!("{}", t!("extensions.dscextension.importNotSupported", file = file, extension = self.type_name));
return Err(DscError::NotSupported(
t!("extensions.dscextension.importNotSupported", file = file, extension = self.type_name).to_string(),
));
}

let extension = match serde_json::from_value::<ExtensionManifest>(self.manifest.clone()) {
Ok(manifest) => manifest,
Err(err) => {
return Err(DscError::Manifest(self.type_name.clone(), err));
}
};
let Some(import) = extension.import else {
return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Import.to_string()));
};
let args = process_import_args(import.args.as_ref(), file)?;
let (_exit_code, stdout, _stderr) = invoke_command(
&import.executable,
args,
None,
Some(self.directory.as_str()),
None,
extension.exit_codes.as_ref(),
)?;
if stdout.is_empty() {
info!("{}", t!("extensions.dscextension.importNoResults", extension = self.type_name));
} else {
return Ok(stdout);
}
}
Err(DscError::UnsupportedCapability(
self.type_name.clone(),
Capability::Import.to_string()
))
}

/// Retrieve a secret using the extension.
///
/// # Arguments
///
/// * `name` - The name of the secret to retrieve.
/// * `vault` - An optional vault name to use for the secret.
///
/// # Returns
///
/// A result containing the secret as a string or an error.
///
/// # Errors
///
/// This function will return an error if the secret retrieval fails or if the extension does not support the secret capability.
pub fn secret(&self, name: &str, vault: Option<&str>) -> Result<Option<String>, DscError> {
if self.capabilities.contains(&Capability::Secret) {
debug!("{}", t!("extensions.dscextension.retrievingSecretFromExtension", name = name, extension = self.type_name));
let extension = match serde_json::from_value::<ExtensionManifest>(self.manifest.clone()) {
Ok(manifest) => manifest,
Err(err) => {
return Err(DscError::Manifest(self.type_name.clone(), err));
}
};
let Some(secret) = extension.secret else {
return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Secret.to_string()));
};
let args = process_secret_args(secret.args.as_ref(), name, vault);
let (_exit_code, stdout, _stderr) = invoke_command(
&secret.executable,
args,
vault,
Some(self.directory.as_str()),
None,
extension.exit_codes.as_ref(),
)?;
if stdout.is_empty() {
debug!("{}", t!("extensions.dscextension.extensionReturnedNoSecret", extension = self.type_name));
Ok(None)
} else {
// see if multiple lines were returned
let secret = if stdout.lines().count() > 1 {
return Err(DscError::NotSupported(t!("extensions.dscextension.secretMultipleLinesReturned", extension = self.type_name).to_string()));
} else {
debug!("{}", t!("extensions.dscextension.extensionReturnedSecret", extension = self.type_name));
// remove any trailing newline characters
stdout.trim_end_matches('\n').to_string()
};
Ok(Some(secret))
}
} else {
Err(DscError::UnsupportedCapability(
self.type_name.clone(),
Capability::Secret.to_string()
))
}
}
}

impl Default for DscExtension {
fn default() -> Self {
DscExtension::new()
}
}

fn process_import_args(args: Option<&Vec<ImportArgKind>>, file: &str) -> Result<Option<Vec<String>>, DscError> {
let Some(arg_values) = args else {
debug!("{}", t!("dscresources.commandResource.noArgs"));
return Ok(None);
};

// make path absolute
let path = Path::new(file);
let Ok(full_path) = path.absolutize() else {
return Err(DscError::Extension(t!("util.failedToAbsolutizePath", path = path : {:?}).to_string()));
};

let mut processed_args = Vec::<String>::new();
for arg in arg_values {
match arg {
ImportArgKind::String(s) => {
processed_args.push(s.clone());
},
ImportArgKind::File { file_arg } => {
if !file_arg.is_empty() {
processed_args.push(file_arg.to_string());
}
processed_args.push(full_path.to_string_lossy().to_string());
},
}
}

Ok(Some(processed_args))
}

fn process_secret_args(args: Option<&Vec<SecretArgKind>>, name: &str, vault: Option<&str>) -> Option<Vec<String>> {
let Some(arg_values) = args else {
debug!("{}", t!("dscresources.commandResource.noArgs"));
return None;
};

let mut processed_args = Vec::<String>::new();
for arg in arg_values {
match arg {
SecretArgKind::String(s) => {
processed_args.push(s.clone());
},
SecretArgKind::Name { name_arg } => {
processed_args.push(name_arg.to_string());
processed_args.push(name.to_string());
},
SecretArgKind::Vault { vault_arg } => {
if let Some(value) = vault {
processed_args.push(vault_arg.to_string());
processed_args.push(value.to_string());
}
},
}
}

Some(processed_args)
}
Loading
Loading