Skip to content

Commit

Permalink
feat!: add support for secrets in manifests
Browse files Browse the repository at this point in the history
This adds support for secrets in wasmCloud application manifests. The
secrets themselves are actually _secret references_ as outlined in
wasmCloud/wasmCloud#2190. Just like config, secrets can be specified at
the component or provider level or on a link.

Secret references themselves are actually implemented as an additional
kind of config stored in the same config data bucket. However, I opted
to implement a dedicated scaler for secrets that is largely a clone of
the existing ConfigScaler since the underlying data type is very
different than the arbitrary set of key/value pairs we use for config.

An example of what this looks like in a component is shown below:

```yaml
spec:
  components:
    - name: http-component
      type: component
      properties:
        image: ghcr.io/wasmcloud/test-fetch-with-token:0.1.0-fake
        secrets:
          - name: some-api-token
            source:
              backend: nats-kv
              key: test-value
              version: 1
          - name: my-other-secret
            source:
              backend: aws-secrets-manager
              value: secret-name
              version: "be01a5fb-7ebb-4ae9-8ea0-0902e8940bc0"
```

This contains a breaking change to the way that we specify config on
links:

```yaml
- type: link
  properties:
    namespace: wasmcloud
    package: postgres
    interfaces: [managed-query]
    target:
      name: sql-postgres
      secrets:
        - name: db-password
          source:
            backend: nats-kv
            key: myapp_db-password
            version: 1
```

Instead of using `target_config` and `source_config`, this renames them
to `target` and `source` respectively and adds keys for `config` and
`secrets`. The name of the target and source are now keys at the top
level of the `source` and `target` blocks, as seen above.

Signed-off-by: Dan Norris <protochron@users.noreply.github.com>
  • Loading branch information
protochron committed Jul 11, 2024
1 parent 61c45e1 commit 14229b6
Show file tree
Hide file tree
Showing 18 changed files with 673 additions and 80 deletions.
146 changes: 130 additions & 16 deletions crates/wadm-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ pub const LINK_TRAIT: &str = "link";
/// for a manifest
pub const LATEST_VERSION: &str = "latest";

pub const SECRET_TYPE: &str = "v1.secret.wasmcloud.dev";

/// An OAM manifest
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, utoipa::ToSchema)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -135,13 +137,14 @@ pub struct Specification {
pub policies: Vec<Policy>,
}

/// A policy definition
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Policy {
/// The name of the policy
/// The name of this policy
pub name: String,
/// The properties for this policy
pub properties: BTreeMap<String, String>,
/// The type of the policy
#[serde(rename = "type")]
pub policy_type: String,
}
Expand Down Expand Up @@ -186,6 +189,90 @@ pub struct ComponentProperties {
/// these values at runtime using `wasi:runtime/config.`
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
/// Named secret references to pass to the component. The component will be able to retrieve
/// these values at runtime using `wasmcloud:secrets/store`.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
pub struct ConfigDefinition {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct SecretProperty {
/// The name of the secret. This is used by a reference by the component or capability to
/// get the secret value as a resource.
pub name: String,
/// The source of the secret. This indicates how to retrieve the secret value from a secrets
/// backend and which backend to actually query.
pub source: SecretSourceProperty,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
pub struct SecretSourceProperty {
/// The backend to use for retrieving the secret.
pub backend: String,
/// The key to use for retrieving the secret from the backend.
pub key: String,
/// The version of the secret to retrieve. If not supplied, the latest version will be used.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
}

impl TryFrom<HashMap<String, String>> for SecretSourceProperty {
type Error = anyhow::Error;

// TODO should this actually just wrap serde_json?
fn try_from(value: HashMap<String, String>) -> Result<Self, Self::Error> {
let secret_type = value
.get("type")
.ok_or_else(|| anyhow::anyhow!("Secret source must have a type"))?;

// Do we actually care? Feels like we should use a proto or something if we do since
// versioning would be a lot easier
if secret_type != SECRET_TYPE {
return Err(anyhow::anyhow!(
"Secret source type must be {}",
SECRET_TYPE
));
}

let backend = value
.get("backend")
.ok_or_else(|| anyhow::anyhow!("Secret source must have a backend"))?;

let key = value
.get("key")
.ok_or_else(|| anyhow::anyhow!("Secret source must have a key"))?;

let version = value.get("version").cloned();

Ok(Self {
backend: backend.clone(),
key: key.clone(),
version,
})
}
}

impl TryInto<HashMap<String, String>> for SecretSourceProperty {
type Error = anyhow::Error;

fn try_into(self) -> Result<HashMap<String, String>, Self::Error> {
let mut map = HashMap::new();
map.insert("type".to_string(), SECRET_TYPE.to_string());
map.insert("backend".to_string(), self.backend);
map.insert("key".to_string(), self.key);
if let Some(version) = self.version {
map.insert("version".to_string(), version);
}
Ok(map)
}
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
Expand All @@ -201,6 +288,10 @@ pub struct CapabilityProperties {
/// to the provider at runtime using the provider SDK's `init()` function.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
/// Named secret references to pass to the t. The provider will be able to retrieve
/// these values at runtime using `wasmcloud:secrets/store`.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -311,25 +402,37 @@ impl PartialEq<ConfigProperty> for String {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct LinkProperty {
/// The target this link applies to. This should be the name of a component in the manifest
pub target: String,
/// WIT namespace for the link
pub namespace: String,
/// WIT package for the link
pub package: String,
/// WIT interfaces for the link
pub interfaces: Vec<String>,
/// Configuration to apply to the source of the link
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub source_config: Vec<ConfigProperty>,
pub source: Option<ConfigDefinition>,
/// Configuration to apply to the target of the link
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub target_config: Vec<ConfigProperty>,
pub target: TargetConfig,
/// The name of this link
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
pub struct TargetConfig {
/// The target this link applies to. This should be the name of a component in the manifest
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub config: Vec<ConfigProperty>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secrets: Vec<SecretProperty>,
}

impl PartialEq<TargetConfig> for String {
fn eq(&self, other: &TargetConfig) -> bool {
self == &other.name
}
}

/// Properties for spread scalers
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -521,8 +624,8 @@ mod test {
"Should have link property"
);
if let TraitProperty::Link(ld) = &traits[0].properties {
assert_eq!(ld.source_config, vec![]);
assert_eq!(ld.target, "userinfo".to_string());
assert_eq!(ld.source.config, vec![]);
assert_eq!(ld.target.name, "userinfo".to_string());
} else {
panic!("trait property was not a link definition");
}
Expand Down Expand Up @@ -551,15 +654,22 @@ mod test {
let trait_item = Trait::new_spreadscaler(spreadscalerprop);
trait_vec.push(trait_item);
let linkdefprop = LinkProperty {
target: "webcap".to_string(),
target: TargetConfig {
name: "webcap".to_string(),
..Default::default()
},
namespace: "wasi".to_string(),
package: "http".to_string(),
interfaces: vec!["incoming-handler".to_string()],
source_config: vec![ConfigProperty {
name: "http".to_string(),
properties: Some(HashMap::from([("port".to_string(), "8080".to_string())])),
}],
target_config: vec![],
source: ConfigDefinition {
config: {
vec![ConfigProperty {
name: "http".to_string(),
properties: Some(HashMap::from([("port".to_string(), "8080".to_string())])),
}]
},
..Default::default()
},
name: Some("default".to_string()),
};
let trait_item = Trait::new_link(linkdefprop);
Expand All @@ -572,6 +682,7 @@ mod test {
image: "wasmcloud.azurecr.io/fake:1".to_string(),
id: None,
config: vec![],
secrets: vec![],
},
},
traits: Some(trait_vec),
Expand All @@ -584,6 +695,7 @@ mod test {
image: "wasmcloud.azurecr.io/httpserver:0.13.1".to_string(),
id: None,
config: vec![],
secrets: vec![],
},
},
traits: None,
Expand Down Expand Up @@ -611,6 +723,7 @@ mod test {
image: "wasmcloud.azurecr.io/ledblinky:0.0.1".to_string(),
id: None,
config: vec![],
secrets: vec![],
},
},
traits: Some(trait_vec),
Expand All @@ -619,6 +732,7 @@ mod test {

let spec = Specification {
components: component_vec,
policies: vec![],
};
let metadata = Metadata {
name: "my-example-app".to_string(),
Expand Down
21 changes: 15 additions & 6 deletions crates/wadm-types/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,8 +362,16 @@ fn check_dangling_links(manifest: &Manifest) -> Vec<ValidationFailure> {
for link_trait in manifest.links() {
match &link_trait.properties {
TraitProperty::Custom(obj) => {
// Ensure target property it present
match obj["target"].as_str() {
if obj.get("target").is_none() {
failures.push(ValidationFailure::new(
ValidationFailureLevel::Error,
"custom link is missing 'target' property".into(),
));
continue;
}

// Ensure target property is present
match obj["target"]["name"].as_str() {
// If target is present, ensure it's pointing to a known component
Some(target) if !lookup.contains_key(&String::from(target)) => {
failures.push(ValidationFailure::new(
Expand All @@ -376,7 +384,7 @@ fn check_dangling_links(manifest: &Manifest) -> Vec<ValidationFailure> {
// if target property is not present, note that it is missing
None => failures.push(ValidationFailure::new(
ValidationFailureLevel::Error,
"custom link is missing 'target' property".into(),
"custom link is missing 'target' name property".into(),
)),
}
}
Expand All @@ -385,12 +393,13 @@ fn check_dangling_links(manifest: &Manifest) -> Vec<ValidationFailure> {
let link_identifier = name
.as_ref()
.map(|n| format!("(name [{n}])"))
.unwrap_or_else(|| format!("(target [{target}])"));
if !lookup.contains_key(target) {
.unwrap_or_else(|| format!("(target [{}])", target.name));
if !lookup.contains_key(&target.name) {
failures.push(ValidationFailure::new(
ValidationFailureLevel::Warning,
format!(
"link {link_identifier} target [{target}] is not a listed component"
"link {link_identifier} target [{}] is not a listed component",
target.name
),
))
}
Expand Down
2 changes: 1 addition & 1 deletion crates/wadm/src/scaler/configscaler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use tracing::instrument;
use tracing::trace;
use wadm_types::{
api::{StatusInfo, StatusType},
TraitProperty,
SecretSourceProperty, TraitProperty,
};

use crate::commands::{DeleteConfig, PutConfig};
Expand Down
Loading

0 comments on commit 14229b6

Please sign in to comment.