Skip to content

Commit 3c34ade

Browse files
committed
Add extensions to context, add tests
1 parent 55b7425 commit 3c34ade

File tree

15 files changed

+291
-45
lines changed

15 files changed

+291
-45
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
Describe 'Tests for the secret() function and extensions' {
5+
BeforeAll {
6+
$oldPath = $env:PATH
7+
$toolPath = Resolve-Path -Path "$PSScriptRoot/../../extensions/test/secret"
8+
$env:PATH = "$toolPath" + [System.IO.Path]::PathSeparator + $oldPath
9+
}
10+
11+
AfterAll {
12+
$env:PATH = $oldPath
13+
}
14+
15+
It 'Call secret() function with just a name' {
16+
$configYaml = @'
17+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
18+
resources:
19+
- name: Echo
20+
type: Microsoft.DSC.Debug/Echo
21+
properties:
22+
output: "[secret('MySecret')]"
23+
'@
24+
$out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json
25+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/error.log)
26+
$out.results.Count | Should -Be 1
27+
$out.results[0].result.actualState.Output | Should -BeExactly 'Hello'
28+
}
29+
30+
It 'Call secret() function with a name and vault' {
31+
$configYaml = @'
32+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
33+
resources:
34+
- name: Echo
35+
type: Microsoft.DSC.Debug/Echo
36+
properties:
37+
output: "[secret('DifferentSecret', 'VaultA')]"
38+
'@
39+
$out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json
40+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/error.log)
41+
$out.results.Count | Should -Be 1
42+
$out.results[0].result.actualState.Output | Should -BeExactly 'Hello2'
43+
}
44+
45+
It 'Call secret() function with a name that does not exist' {
46+
$configYaml = @'
47+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
48+
resources:
49+
- name: Echo
50+
type: Microsoft.DSC.Debug/Echo
51+
properties:
52+
output: "[secret('NonExistentSecret')]"
53+
'@
54+
dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json
55+
$LASTEXITCODE | Should -Be 2
56+
$errorMessage = Get-Content -Raw -Path $TestDrive/error.log
57+
$errorMessage | Should -Match "Secret 'NonExistentSecret' not found"
58+
}
59+
60+
It 'Call secret() function with a vault that does not exist' {
61+
$configYaml = @'
62+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
63+
resources:
64+
- name: Echo
65+
type: Microsoft.DSC.Debug/Echo
66+
properties:
67+
output: "[secret('MySecret', 'NonExistentVault')]"
68+
'@
69+
dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json
70+
$LASTEXITCODE | Should -Be 2
71+
$errorMessage = Get-Content -Raw -Path $TestDrive/error.log
72+
$errorMessage | Should -Match "Secret 'MySecret' not found"
73+
}
74+
75+
It 'Call secret() function with a duplicate secret' {
76+
$configYaml = @'
77+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
78+
resources:
79+
- name: Echo
80+
type: Microsoft.DSC.Debug/Echo
81+
properties:
82+
output: "[secret('DuplicateSecret')]"
83+
'@
84+
dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json
85+
$LASTEXITCODE | Should -Be 2
86+
$errorMessage = Get-Content -Raw -Path $TestDrive/error.log
87+
$errorMessage | Should -Match "Multiple secrets with the same name 'DuplicateSecret' was returned, try specifying a vault"
88+
}
89+
90+
It 'Call secret() function with secret and vault to disambiguate' {
91+
$configYaml = @'
92+
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
93+
resources:
94+
- name: Echo
95+
type: Microsoft.DSC.Debug/Echo
96+
properties:
97+
output: "[secret('DuplicateSecret', 'Vault1')]"
98+
'@
99+
$out = dsc -l trace config get -i $configYaml 2> $TestDrive/error.log | ConvertFrom-Json
100+
$LASTEXITCODE | Should -Be 0 -Because (Get-Content -Raw -Path $TestDrive/error.log)
101+
$out.results.Count | Should -Be 1
102+
$out.results[0].result.actualState.Output | Should -BeExactly 'World'
103+
}
104+
}

dsc_lib/locales/en-us.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,10 @@ resourceManifestSchemaDescription = "Defines the JSON Schema the resource manife
174174
discoverNoResults = "No results returned for discovery extension '%{extension}'"
175175
discoverNotAbsolutePath = "Resource path from extension '%{extension}' is not an absolute path: %{path}"
176176
extensionReturned = "Extension '%{extension}' returned line: %{line}"
177+
retrievingSecretFromExtension = "Retrieving secret '%{name}' from extension '%{extension}'"
178+
secretExtensionReturnedInvalidJson = "Extension '%{extension}' returned invalid JSON: %{error}"
179+
extensionReturnedSecret = "Extension '%{extension}' returned secret"
180+
extensionReturnedNoSecret = "Extension '%{extension}' did not return a secret"
177181

178182
[extensions.extension_manifest]
179183
extensionManifestSchemaTitle = "Extension manifest schema URI"
@@ -269,6 +273,13 @@ invalidFirstArgType = "Invalid argument type for first parameter"
269273
incorrectNameFormat = "Name argument cannot contain a slash"
270274
invalidSecondArgType = "Invalid argument type for second parameter"
271275

276+
[functions.secret]
277+
notString = "Parameter secret name is not a string"
278+
multipleSecrets = "Multiple secrets with the same name '%{name}' was returned, try specifying a vault"
279+
extensionReturnedError = "Extension '%{extension}': %{error}"
280+
noExtensions = "No extensions supporting secrets was found"
281+
secretNotFound = "Secret '%{name}' not found"
282+
272283
[functions.sub]
273284
invoked = "sub function"
274285

@@ -329,7 +340,6 @@ manifestDescription = "manifest description"
329340
commandOperation = "Command: Operation"
330341
forExecutable = "for executable"
331342
function = "Function"
332-
error = "error"
333343
integerConversion = "Function integer argument conversion"
334344
invalidConfiguration = "Invalid configuration"
335345
unsupportedManifestVersion = "Unsupported manifest version"

dsc_lib/src/configure/context.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Licensed under the MIT License.
33

44
use chrono::{DateTime, Local};
5-
use crate::configure::config_doc::ExecutionKind;
5+
use crate::{configure::config_doc::ExecutionKind, extensions::dscextension::DscExtension};
66
use security_context_lib::{get_security_context, SecurityContext};
77
use serde_json::{Map, Value};
88
use std::{collections::HashMap, path::PathBuf};
@@ -11,6 +11,7 @@ use super::config_doc::{DataType, SecurityContextKind};
1111

1212
pub struct Context {
1313
pub execution_type: ExecutionKind,
14+
pub extensions: Vec<DscExtension>,
1415
pub references: Map<String, Value>,
1516
pub system_root: PathBuf,
1617
pub parameters: HashMap<String, (Value, DataType)>,
@@ -24,6 +25,7 @@ impl Context {
2425
pub fn new() -> Self {
2526
Self {
2627
execution_type: ExecutionKind::Actual,
28+
extensions: Vec::new(),
2729
references: Map::new(),
2830
system_root: get_default_os_system_root(),
2931
parameters: HashMap::new(),

dsc_lib/src/configure/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,11 +233,14 @@ impl Configurator {
233233
json: json.to_owned(),
234234
config: Configuration::new(),
235235
context: Context::new(),
236-
discovery,
236+
discovery: discovery.clone(),
237237
statement_parser: Statement::new()?,
238238
progress_format,
239239
};
240240
config.validate_config()?;
241+
for extension in discovery.extensions.values() {
242+
config.context.extensions.push(extension.clone());
243+
}
241244
Ok(config)
242245
}
243246

@@ -641,6 +644,7 @@ impl Configurator {
641644
let config = serde_json::from_str::<Configuration>(self.json.as_str())?;
642645
self.set_parameters(parameters_input, &config)?;
643646
self.set_variables(&config)?;
647+
self.context.extensions = self.discovery.extensions.values().cloned().collect();
644648
Ok(())
645649
}
646650

dsc_lib/src/discovery/command_discovery.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub enum ImportedManifest {
3636
Extension(DscExtension),
3737
}
3838

39+
#[derive(Clone)]
3940
pub struct CommandDiscovery {
4041
// use BTreeMap so that the results are sorted by the typename, the Vec is sorted by version
4142
adapters: BTreeMap<String, Vec<DscResource>>,
@@ -79,6 +80,11 @@ impl CommandDiscovery {
7980
}
8081
}
8182

83+
#[must_use]
84+
pub fn get_extensions(&self) -> &BTreeMap<String, DscExtension> {
85+
&self.extensions
86+
}
87+
8288
fn get_resource_path_setting() -> Result<ResourcePathSetting, DscError>
8389
{
8490
if let Ok(v) = get_setting("resourcePath") {
@@ -546,6 +552,13 @@ impl ResourceDiscovery for CommandDiscovery {
546552
}
547553
Ok(found_resources)
548554
}
555+
556+
fn get_extensions(&mut self) -> Result<BTreeMap<String, DscExtension>, DscError> {
557+
if self.extensions.is_empty() {
558+
self.discover(&DiscoveryKind::Extension, "*")?;
559+
}
560+
Ok(self.extensions.clone())
561+
}
549562
}
550563

551564
// TODO: This should be a BTreeMap of the resource name and a BTreeMap of the version and DscResource, this keeps it version sorted more efficiently
@@ -709,6 +722,10 @@ fn load_extension_manifest(path: &Path, manifest: &ExtensionManifest) -> Result<
709722
verify_executable(&manifest.r#type, "discover", &discover.executable);
710723
capabilities.push(dscextension::Capability::Discover);
711724
}
725+
if let Some(secret) = &manifest.secret {
726+
verify_executable(&manifest.r#type, "secret", &secret.executable);
727+
capabilities.push(dscextension::Capability::Secret);
728+
}
712729

713730
let extension = DscExtension {
714731
type_name: manifest.r#type.clone(),

dsc_lib/src/discovery/discovery_trait.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4-
use crate::{dscerror::DscError, dscresources::dscresource::DscResource};
4+
use crate::{dscerror::DscError, extensions::dscextension::DscExtension, dscresources::dscresource::DscResource};
55
use std::collections::BTreeMap;
66

77
use super::command_discovery::ImportedManifest;
@@ -77,4 +77,15 @@ pub trait ResourceDiscovery {
7777
///
7878
/// This function will return an error if the underlying discovery fails.
7979
fn find_resources(&mut self, required_resource_types: &[String]) -> Result<BTreeMap<String, DscResource>, DscError>;
80+
81+
/// Get the available extensions.
82+
///
83+
/// # Returns
84+
///
85+
/// A result containing a map of extension names to their corresponding `DscExtension` instances.
86+
///
87+
/// # Errors
88+
///
89+
/// This function will return an error if the underlying discovery fails.
90+
fn get_extensions(&mut self) -> Result<BTreeMap<String, DscExtension>, DscError>;
8091
}

dsc_lib/src/discovery/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use crate::discovery::discovery_trait::{DiscoveryKind, ResourceDiscovery};
88
use crate::extensions::dscextension::DscExtension;
99
use crate::{dscresources::dscresource::DscResource, dscerror::DscError, progress::ProgressFormat};
1010
use std::collections::BTreeMap;
11-
use command_discovery::ImportedManifest;
11+
use command_discovery::{CommandDiscovery, ImportedManifest};
1212
use tracing::error;
1313

1414
#[derive(Clone)]
@@ -80,8 +80,9 @@ impl Discovery {
8080
///
8181
/// * `required_resource_types` - The required resource types.
8282
pub fn find_resources(&mut self, required_resource_types: &[String], progress_format: ProgressFormat) {
83+
let command_discovery = CommandDiscovery::new(progress_format);
8384
let discovery_types: Vec<Box<dyn ResourceDiscovery>> = vec![
84-
Box::new(command_discovery::CommandDiscovery::new(progress_format)),
85+
Box::new(command_discovery),
8586
];
8687
let mut remaining_required_resource_types = required_resource_types.to_owned();
8788
for mut discovery_type in discovery_types {
@@ -98,6 +99,9 @@ impl Discovery {
9899
self.resources.insert(resource.0.clone(), resource.1);
99100
remaining_required_resource_types.retain(|x| *x != resource.0);
100101
};
102+
if let Ok(extensions) = discovery_type.get_extensions() {
103+
self.extensions.extend(extensions);
104+
}
101105
}
102106
}
103107
}

dsc_lib/src/dscerror.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ pub enum DscError {
3232
#[error("{0}")]
3333
Extension(String),
3434

35-
#[error("{t} '{0}' {t2}: {1}", t = t!("dscerror.function"), t2 = t!("dscerror.error"))]
35+
#[error("{t} '{0}': {1}", t = t!("dscerror.function"))]
3636
Function(String, String),
3737

38-
#[error("{t} '{0}' {t2}: {1}", t = t!("dscerror.function"), t2 = t!("dscerror.error"))]
38+
#[error("{t} '{0}': {1}", t = t!("dscerror.function"))]
3939
FunctionArg(String, String),
4040

4141
#[error("{t}: {0}", t = t!("dscerror.integerConversion"))]

dsc_lib/src/extensions/dscextension.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ impl DscExtension {
145145
/// This function will return an error if the secret retrieval fails or if the extension does not support the secret capability.
146146
pub fn secret(&self, name: &str, vault: Option<&str>) -> Result<Option<String>, DscError> {
147147
if self.capabilities.contains(&Capability::Secret) {
148+
debug!("{}", t!("extensions.dscextension.retrievingSecretFromExtension", name = name, extension = self.type_name));
148149
let extension = match serde_json::from_value::<ExtensionManifest>(self.manifest.clone()) {
149150
Ok(manifest) => manifest,
150151
Err(err) => {
@@ -170,10 +171,15 @@ impl DscExtension {
170171
let result: SecretResult = match serde_json::from_str(&stdout) {
171172
Ok(value) => value,
172173
Err(err) => {
173-
return Err(DscError::Json(err));
174+
return Err(DscError::Extension(t!("extensions.dscextension.secretExtensionReturnedInvalidJson", extension = self.type_name, error = err).to_string()));
174175
}
175176
};
176-
Ok(Some(result.secret))
177+
if result.secret.is_some() {
178+
debug!("{}", t!("extensions.dscextension.extensionReturnedSecret", extension = self.type_name));
179+
} else {
180+
debug!("{}", t!("extensions.dscextension.extensionReturnedNoSecret", extension = self.type_name));
181+
}
182+
Ok(result.secret)
177183
}
178184
} else {
179185
Err(DscError::UnsupportedCapability(

dsc_lib/src/extensions/secret.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,5 @@ pub struct SecretMethod {
3333

3434
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
3535
pub struct SecretResult {
36-
pub secret: String,
36+
pub secret: Option<String>,
3737
}

0 commit comments

Comments
 (0)