diff --git a/dsc_lib/src/extensions/discover.rs b/dsc_lib/src/extensions/discover.rs index d38aa9945..1a716565e 100644 --- a/dsc_lib/src/extensions/discover.rs +++ b/dsc_lib/src/extensions/discover.rs @@ -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 { @@ -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, DscError> { + let mut resources: Vec = Vec::new(); + + if self.capabilities.contains(&Capability::Discover) { + let extension = match serde_json::from_value::(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() + )) + } + } +} diff --git a/dsc_lib/src/extensions/dscextension.rs b/dsc_lib/src/extensions/dscextension.rs index 3dacfd572..d7b01c5d3 100644 --- a/dsc_lib/src/extensions/dscextension.rs +++ b/dsc_lib/src/extensions/dscextension.rs @@ -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)] @@ -87,181 +67,6 @@ 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, DscError> { - let mut resources: Vec = Vec::new(); - - if self.capabilities.contains(&Capability::Discover) { - let extension = match serde_json::from_value::(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 { - 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::(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, 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::(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 { @@ -269,61 +74,3 @@ impl Default for DscExtension { DscExtension::new() } } - -fn process_import_args(args: Option<&Vec>, file: &str) -> Result>, 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::::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>, name: &str, vault: Option<&str>) -> Option> { - let Some(arg_values) = args else { - debug!("{}", t!("dscresources.commandResource.noArgs")); - return None; - }; - - let mut processed_args = Vec::::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) -} diff --git a/dsc_lib/src/extensions/import.rs b/dsc_lib/src/extensions/import.rs index 7b7c21f65..79e83dd94 100644 --- a/dsc_lib/src/extensions/import.rs +++ b/dsc_lib/src/extensions/import.rs @@ -1,8 +1,27 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::{ + dscerror::DscError, + dscresources::{ + command_resource::{ + invoke_command, + }, + }, + extensions::{ + dscextension::{ + Capability, + DscExtension, + }, + extension_manifest::ExtensionManifest, + }, +}; +use path_absolutize::Absolutize; +use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::path::Path; +use tracing::{debug, info}; #[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] pub struct ImportMethod { @@ -27,3 +46,91 @@ pub enum ImportArgKind { file_arg: String, }, } + +impl DscExtension { + /// 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 { + 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::(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() + )) + } +} + +fn process_import_args(args: Option<&Vec>, file: &str) -> Result>, 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::::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)) +} diff --git a/dsc_lib/src/extensions/secret.rs b/dsc_lib/src/extensions/secret.rs index 8c52d42aa..01d7fe3b2 100644 --- a/dsc_lib/src/extensions/secret.rs +++ b/dsc_lib/src/extensions/secret.rs @@ -1,8 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use crate::{ + dscerror::DscError, + dscresources::{ + command_resource::invoke_command, + }, + extensions::{ + dscextension::{ + Capability, + DscExtension, + }, + extension_manifest::ExtensionManifest, + }, +}; +use rust_i18n::t; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use tracing::debug; #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)] #[serde(untagged)] @@ -30,3 +45,90 @@ pub struct SecretMethod { /// The arguments to pass to the command to perform a Get. pub args: Option>, } + +impl DscExtension { + /// 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, 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::(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() + )) + } + } +} + +fn process_secret_args(args: Option<&Vec>, name: &str, vault: Option<&str>) -> Option> { + let Some(arg_values) = args else { + debug!("{}", t!("dscresources.commandResource.noArgs")); + return None; + }; + + let mut processed_args = Vec::::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) +}