Skip to content

Commit c81a939

Browse files
committed
Initial support for secret function and extension
1 parent 39eade5 commit c81a939

File tree

6 files changed

+185
-8
lines changed

6 files changed

+185
-8
lines changed

dsc/src/subcommand.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,8 +645,10 @@ fn list_extensions(dsc: &mut DscManager, extension_name: Option<&String>, format
645645
let mut include_separator = false;
646646
for manifest_resource in dsc.list_available(&DiscoveryKind::Extension, extension_name.unwrap_or(&String::from("*")), "", progress_format) {
647647
if let ImportedManifest::Extension(extension) = manifest_resource {
648+
let mut capabilities = "--".to_string();
648649
let capability_types = [
649650
(ExtensionCapability::Discover, "d"),
651+
(ExtensionCapability::Secret, "s"),
650652
];
651653
let mut capabilities = "-".repeat(capability_types.len());
652654

dsc_lib/src/extensions/dscextension.rs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ use serde::{Deserialize, Serialize};
66
use serde_json::Value;
77
use schemars::JsonSchema;
88
use std::{fmt::Display, path::Path};
9-
use tracing::{info, trace};
9+
use tracing::{debug, info, trace};
1010

1111
use crate::{discovery::command_discovery::{load_manifest, ImportedManifest}, dscerror::DscError, dscresources::{command_resource::{invoke_command, process_args}, dscresource::DscResource}};
1212

13-
use super::{discover::DiscoverResult, extension_manifest::ExtensionManifest};
13+
use super::{discover::DiscoverResult, extension_manifest::ExtensionManifest, secret::SecretArgKind};
1414

1515
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
1616
#[serde(deny_unknown_fields)]
@@ -39,12 +39,15 @@ pub struct DscExtension {
3939
pub enum Capability {
4040
/// The extension aids in discovering resources.
4141
Discover,
42+
/// The extension aids in retrieving secrets.
43+
Secret,
4244
}
4345

4446
impl Display for Capability {
4547
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4648
match self {
4749
Capability::Discover => write!(f, "Discover"),
50+
Capability::Secret => write!(f, "Secret"),
4851
}
4952
}
5053
}
@@ -125,10 +128,75 @@ impl DscExtension {
125128
))
126129
}
127130
}
131+
132+
pub fn secret(&self, name: &str, vault: Option<&str>) -> Result<Value, DscError> {
133+
if self.capabilities.contains(&Capability::Secret) {
134+
let extension = match serde_json::from_value::<ExtensionManifest>(self.manifest.clone()) {
135+
Ok(manifest) => manifest,
136+
Err(err) => {
137+
return Err(DscError::Manifest(self.type_name.clone(), err));
138+
}
139+
};
140+
let Some(secret) = extension.secret else {
141+
return Err(DscError::UnsupportedCapability(self.type_name.clone(), Capability::Secret.to_string()));
142+
};
143+
let args = process_secret_args(secret.args.as_ref(), name, vault);
144+
let (_exit_code, stdout, _stderr) = invoke_command(
145+
&secret.executable,
146+
args,
147+
vault,
148+
Some(self.directory.as_str()),
149+
None,
150+
extension.exit_codes.as_ref(),
151+
)?;
152+
if stdout.is_empty() {
153+
info!("{}", t!("extensions.dscextension.secretNoResults", extension = self.type_name));
154+
Ok(Value::Null)
155+
} else {
156+
match serde_json::from_str(&stdout) {
157+
Ok(value) => Ok(value),
158+
Err(err) => Err(DscError::Json(err)),
159+
}
160+
}
161+
} else {
162+
Err(DscError::UnsupportedCapability(
163+
self.type_name.clone(),
164+
Capability::Secret.to_string()
165+
))
166+
}
167+
}
128168
}
129169

130170
impl Default for DscExtension {
131171
fn default() -> Self {
132172
DscExtension::new()
133173
}
134174
}
175+
176+
fn process_secret_args(args: Option<&Vec<SecretArgKind>>, name: &str, vault: Option<&str>) -> Option<Vec<String>> {
177+
let Some(arg_values) = args else {
178+
debug!("{}", t!("dscresources.commandResource.noArgs"));
179+
return None;
180+
};
181+
182+
let mut processed_args = Vec::<String>::new();
183+
for arg in arg_values {
184+
match arg {
185+
SecretArgKind::String(s) => {
186+
processed_args.push(s.clone());
187+
},
188+
SecretArgKind::Name { name_arg } => {
189+
processed_args.push(name_arg.to_string());
190+
processed_args.push(name.to_string());
191+
},
192+
SecretArgKind::Vault { vault_arg } => {
193+
if let Some(value) = vault {
194+
processed_args.push(vault_arg.to_string());
195+
processed_args.push(value.to_string());
196+
}
197+
},
198+
}
199+
}
200+
201+
Some(processed_args)
202+
}

dsc_lib/src/extensions/extension_manifest.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,28 @@ use serde_json::Value;
99
use std::collections::HashMap;
1010

1111
use crate::{dscerror::DscError, schemas::DscRepoSchema};
12-
use crate::extensions::discover::DiscoverMethod;
12+
use crate::extensions::{discover::DiscoverMethod, secret::SecretMethod};
1313

1414
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
1515
#[serde(deny_unknown_fields)]
1616
pub struct ExtensionManifest {
17-
/// The version of the resource manifest schema.
17+
/// The version of the extension manifest schema.
1818
#[serde(rename = "$schema")]
1919
#[schemars(schema_with = "ExtensionManifest::recognized_schema_uris_subschema")]
2020
pub schema_version: String,
2121
/// The namespaced name of the extension.
2222
#[serde(rename = "type")]
2323
pub r#type: String,
24-
/// The version of the resource using semantic versioning.
24+
/// The version of the extension using semantic versioning.
2525
pub version: String,
26-
/// The description of the resource.
26+
/// The description of the extension.
2727
pub description: Option<String>,
28-
/// Tags for the resource.
28+
/// Tags for the extension.
2929
pub tags: Option<Vec<String>>,
30-
/// Details how to call the Discover method of the resource.
30+
/// Details how to call the Discover method of the extension.
3131
pub discover: Option<DiscoverMethod>,
32+
/// Details how to call the Secret method of the extension.
33+
pub secret: Option<SecretMethod>,
3234
/// Mapping of exit codes to descriptions. Zero is always success and non-zero is always failure.
3335
#[serde(rename = "exitCodes", skip_serializing_if = "Option::is_none")]
3436
pub exit_codes: Option<HashMap<i32, String>>,

dsc_lib/src/extensions/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
pub mod discover;
55
pub mod dscextension;
66
pub mod extension_manifest;
7+
pub mod secret;

dsc_lib/src/extensions/secret.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use schemars::JsonSchema;
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
8+
#[serde(untagged)]
9+
pub enum SecretArgKind {
10+
/// The argument is a string.
11+
String(String),
12+
/// The argument accepts the secret name.
13+
Name {
14+
/// The argument that accepts the secret name.
15+
#[serde(rename = "nameArg")]
16+
name_arg: String,
17+
},
18+
/// The argument accepts the vault name.
19+
Vault {
20+
/// The argument that accepts the vault name.
21+
#[serde(rename = "vaultArg")]
22+
vault_arg: String,
23+
},
24+
}
25+
26+
#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
27+
pub struct SecretMethod {
28+
/// The command to run to get the state of the resource.
29+
pub executable: String,
30+
/// The arguments to pass to the command to perform a Get.
31+
pub args: Option<Vec<SecretArgKind>>,
32+
}
33+
34+
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
35+
pub struct SecretResult {
36+
pub secret: String,
37+
}

dsc_lib/src/functions/secret.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use crate::DscError;
5+
use crate::configure::context::Context;
6+
use crate::functions::AcceptedArgKind;
7+
use serde_json::Value;
8+
use super::Function;
9+
10+
#[derive(Debug, Default)]
11+
pub struct Secret {}
12+
13+
impl Function for Secret {
14+
fn accepted_arg_types(&self) -> Vec<AcceptedArgKind> {
15+
vec![AcceptedArgKind::String]
16+
}
17+
18+
fn min_args(&self) -> usize {
19+
1
20+
}
21+
22+
fn max_args(&self) -> usize {
23+
2
24+
}
25+
26+
fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> {
27+
let secret = args[0].as_str().ok_or_else(|| {
28+
DscError::InvalidArgumentType("Secret function requires a string argument".to_string())
29+
})?.to_string();
30+
let vault: Option<String> = if args.len() > 1 {
31+
args[1].as_str().map(|s| s.to_string())
32+
} else {
33+
None
34+
};
35+
36+
// if no vault name is provided, we query all extensions supporting the secret method
37+
// to see if any of them can provide the secret. If none can or if multiple can, we return an error.
38+
39+
}
40+
}
41+
42+
#[cfg(test)]
43+
mod tests {
44+
use crate::configure::context::Context;
45+
use crate::parser::Statement;
46+
47+
#[test]
48+
fn strings() {
49+
let mut parser = Statement::new().unwrap();
50+
let result = parser.parse_and_execute("[base64('hello world')]", &Context::new()).unwrap();
51+
assert_eq!(result, "aGVsbG8gd29ybGQ=");
52+
}
53+
54+
#[test]
55+
fn numbers() {
56+
let mut parser = Statement::new().unwrap();
57+
let result = parser.parse_and_execute("[base64(123)]", &Context::new());
58+
assert!(result.is_err());
59+
}
60+
61+
#[test]
62+
fn nested() {
63+
let mut parser = Statement::new().unwrap();
64+
let result = parser.parse_and_execute("[base64(base64('hello world'))]", &Context::new()).unwrap();
65+
assert_eq!(result, "YUdWc2JHOGdkMjl5YkdRPQ==");
66+
}
67+
}

0 commit comments

Comments
 (0)