Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support shared components and providers #381

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 1 addition & 3 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
# TODO: Re-enable the multitenant and upgrades tests in followup to #247
# e2e_test: [e2e_multiple_hosts, e2e_multitenant, e2e_upgrades]
e2e_test: [e2e_multiple_hosts, e2e_upgrades]
e2e_test: [e2e_multiple_hosts, e2e_upgrades, e2e_shared]

steps:
- uses: actions/checkout@v4
Expand Down
31 changes: 29 additions & 2 deletions crates/wadm-types/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ use crate::{
},
CapabilityProperties, Component, ComponentProperties, ConfigDefinition, ConfigProperty,
LinkProperty, Manifest, Metadata, Policy, Properties, SecretProperty, SecretSourceProperty,
Specification, Spread, SpreadScalerProperty, TargetConfig, Trait, TraitProperty,
SharedApplicationComponentProperties, Specification, Spread, SpreadScalerProperty,
TargetConfig, Trait, TraitProperty,
};
use wasmcloud::wadm;
use wasmcloud::wadm::{self};

wit_bindgen_wrpc::generate!({
generate_unused_types: true,
Expand Down Expand Up @@ -87,6 +88,7 @@ impl From<Properties> for wadm::types::Properties {
impl From<ComponentProperties> for wadm::types::ComponentProperties {
fn from(properties: ComponentProperties) -> Self {
wadm::types::ComponentProperties {
application: properties.application.map(Into::into),
image: properties.image,
id: properties.id,
config: properties.config.into_iter().map(|c| c.into()).collect(),
Expand All @@ -98,6 +100,7 @@ impl From<ComponentProperties> for wadm::types::ComponentProperties {
impl From<CapabilityProperties> for wadm::types::CapabilityProperties {
fn from(properties: CapabilityProperties) -> Self {
wadm::types::CapabilityProperties {
application: properties.application.map(Into::into),
image: properties.image,
id: properties.id,
config: properties.config.into_iter().map(|c| c.into()).collect(),
Expand Down Expand Up @@ -135,6 +138,17 @@ impl From<SecretSourceProperty> for wadm::types::SecretSourceProperty {
}
}

impl From<SharedApplicationComponentProperties>
for wadm::types::SharedApplicationComponentProperties
{
fn from(properties: SharedApplicationComponentProperties) -> Self {
wadm::types::SharedApplicationComponentProperties {
name: properties.name,
component: properties.component,
}
}
}

impl From<Trait> for wadm::types::Trait {
fn from(trait_: Trait) -> Self {
wadm::types::Trait {
Expand Down Expand Up @@ -391,6 +405,7 @@ impl From<wadm::types::ComponentProperties> for ComponentProperties {
fn from(properties: wadm::types::ComponentProperties) -> Self {
ComponentProperties {
image: properties.image,
application: properties.application.map(Into::into),
id: properties.id,
config: properties.config.into_iter().map(|c| c.into()).collect(),
secrets: properties.secrets.into_iter().map(|c| c.into()).collect(),
Expand All @@ -402,6 +417,7 @@ impl From<wadm::types::CapabilityProperties> for CapabilityProperties {
fn from(properties: wadm::types::CapabilityProperties) -> Self {
CapabilityProperties {
image: properties.image,
application: properties.application.map(Into::into),
id: properties.id,
config: properties.config.into_iter().map(|c| c.into()).collect(),
secrets: properties.secrets.into_iter().map(|c| c.into()).collect(),
Expand Down Expand Up @@ -438,6 +454,17 @@ impl From<wadm::types::SecretSourceProperty> for SecretSourceProperty {
}
}

impl From<wadm::types::SharedApplicationComponentProperties>
for SharedApplicationComponentProperties
{
fn from(properties: wadm::types::SharedApplicationComponentProperties) -> Self {
SharedApplicationComponentProperties {
name: properties.name,
component: properties.component,
}
}
}

impl From<wadm::types::Trait> for Trait {
fn from(trait_: wadm::types::Trait) -> Self {
Trait {
Expand Down
101 changes: 90 additions & 11 deletions crates/wadm-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub const VERSION_ANNOTATION_KEY: &str = "version";
/// The description key, as predefined by the [OAM
/// spec](https://github.com/oam-dev/spec/blob/master/metadata.md#annotations-format)
pub const DESCRIPTION_ANNOTATION_KEY: &str = "description";
/// The annotation key for shared applications
pub const SHARED_ANNOTATION_KEY: &str = "wasmcloud.dev/shared";
/// The identifier for the builtin spreadscaler trait type
pub const SPREADSCALER_TRAIT: &str = "spreadscaler";
/// The identifier for the builtin daemonscaler trait type
Expand All @@ -34,7 +36,7 @@ pub const LINK_TRAIT: &str = "link";
/// for a manifest
pub const LATEST_VERSION: &str = "latest";

/// An OAM manifest
/// Manifest file based on the Open Application Model (OAM) specification for declaratively managing wasmCloud applications
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, utoipa::ToSchema, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
Expand Down Expand Up @@ -67,11 +69,65 @@ impl Manifest {
.map(|v| v.as_str())
}

/// Indicates if the manifest is shared, meaning it can be used by multiple applications
pub fn shared(&self) -> bool {
self.metadata
.annotations
.get(SHARED_ANNOTATION_KEY)
.is_some_and(|v| v.parse::<bool>().unwrap_or(false))
}

/// Returns the components in the manifest
pub fn components(&self) -> impl Iterator<Item = &Component> {
self.spec.components.iter()
}

/// Helper function to find shared components that are missing from the given list of
/// deployed applications
pub fn missing_shared_components(&self, deployed_apps: &Vec<&Manifest>) -> Vec<&Component> {
self.spec
.components
.iter()
.filter_map(|shared_component| {
match &shared_component.properties {
Properties::Capability {
properties:
CapabilityProperties {
image: None,
application: Some(shared_app),
..
},
}
| Properties::Component {
properties:
ComponentProperties {
image: None,
application: Some(shared_app),
..
},
} => {
if deployed_apps.iter().filter(|a| a.shared()).any(|m| {
m.metadata.name == shared_app.name
&& m.components().any(|c| {
c.name == shared_app.component
// This compares just the enum variant, not the actual properties
// For example, if we reference a shared component that's a capability,
// we want to make sure the deployed component is a capability.
&& std::mem::discriminant(&c.properties)
== std::mem::discriminant(&shared_component.properties)
})
}) {
None
} else {
Some(shared_component)
}
}
_ => None,
}
})
.collect()
}

/// Returns only the WebAssembly components in the manifest
pub fn wasm_components(&self) -> impl Iterator<Item = &Component> {
self.components()
Expand Down Expand Up @@ -154,8 +210,8 @@ pub struct Policy {

/// A component definition
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
// TODO: for some reason this works fine for capapilities but not components
//#[serde(deny_unknown_fields)]
// TODO: figure out why this can't be uncommented
// #[serde(deny_unknown_fields)]
pub struct Component {
/// The name of this component
pub name: String,
Expand Down Expand Up @@ -214,8 +270,14 @@ pub enum Properties {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ComponentProperties {
/// The image reference to use
pub image: String,
/// The image reference to use. Required unless the component is a shared component
/// that is defined in another shared application.
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
/// Information to locate a component within a shared application. Cannot be specified
/// if the image is specified.
#[serde(skip_serializing_if = "Option::is_none")]
pub application: Option<SharedApplicationComponentProperties>,
/// The component ID to use for this component. If not supplied, it will be generated
/// as a combination of the [Metadata::name] and the image reference.
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -266,8 +328,14 @@ pub struct SecretSourceProperty {
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct CapabilityProperties {
/// The image reference to use
pub image: String,
/// The image reference to use. Required unless the component is a shared component
/// that is defined in another shared application.
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
/// Information to locate a component within a shared application. Cannot be specified
/// if the image is specified.
#[serde(skip_serializing_if = "Option::is_none")]
pub application: Option<SharedApplicationComponentProperties>,
/// The component ID to use for this provider. If not supplied, it will be generated
/// as a combination of the [Metadata::name] and the image reference.
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -282,6 +350,14 @@ pub struct CapabilityProperties {
pub secrets: Vec<SecretProperty>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct SharedApplicationComponentProperties {
/// The name of the shared application
pub name: String,
/// The name of the component in the shared application
pub component: String,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct Trait {
Expand Down Expand Up @@ -688,7 +764,7 @@ mod test {
&component.properties,
Properties::Capability {
properties: CapabilityProperties { image, .. }
} if image == "wasmcloud.azurecr.io/httpserver:0.13.1"
} if image.clone().expect("image to be present") == "wasmcloud.azurecr.io/httpserver:0.13.1"
)
})
.expect("Should find capability component")
Expand Down Expand Up @@ -756,7 +832,8 @@ mod test {
name: "userinfo".to_string(),
properties: Properties::Component {
properties: ComponentProperties {
image: "wasmcloud.azurecr.io/fake:1".to_string(),
image: Some("wasmcloud.azurecr.io/fake:1".to_string()),
application: None,
id: None,
config: vec![],
secrets: vec![],
Expand All @@ -769,7 +846,8 @@ mod test {
name: "webcap".to_string(),
properties: Properties::Capability {
properties: CapabilityProperties {
image: "wasmcloud.azurecr.io/httpserver:0.13.1".to_string(),
image: Some("wasmcloud.azurecr.io/httpserver:0.13.1".to_string()),
application: None,
id: None,
config: vec![],
secrets: vec![],
Expand Down Expand Up @@ -797,7 +875,8 @@ mod test {
name: "ledblinky".to_string(),
properties: Properties::Capability {
properties: CapabilityProperties {
image: "wasmcloud.azurecr.io/ledblinky:0.0.1".to_string(),
image: Some("wasmcloud.azurecr.io/ledblinky:0.0.1".to_string()),
application: None,
id: None,
config: vec![],
secrets: vec![],
Expand Down
71 changes: 71 additions & 0 deletions crates/wadm-types/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ pub async fn validate_manifest(manifest: &Manifest) -> Result<Vec<ValidationFail
failures.extend(check_dangling_links(manifest));
failures.extend(validate_policies(manifest));
failures.extend(ensure_no_custom_traits(manifest));
failures.extend(validate_component_properties(manifest));
Ok(failures)
}

Expand Down Expand Up @@ -596,6 +597,76 @@ fn validate_policies(manifest: &Manifest) -> Vec<ValidationFailure> {
failures
}

/// Ensure that all components in a manifest either specify an image reference or a shared
/// component in a different manifest. Note that this does not validate that the image reference
/// is valid or that the shared component is valid, only that one of the two properties is set.
pub fn validate_component_properties(application: &Manifest) -> Vec<ValidationFailure> {
let mut failures = Vec::new();
for component in application.spec.components.iter() {
match &component.properties {
Properties::Component {
properties:
ComponentProperties {
image,
application,
config,
..
},
}
| Properties::Capability {
properties:
CapabilityProperties {
image,
application,
config,
..
},
} => match (image, application) {
(Some(_), Some(_)) => {
failures.push(ValidationFailure::new(
ValidationFailureLevel::Error,
"Component cannot have both 'image' and 'application' properties".into(),
));
}
(None, None) => {
failures.push(ValidationFailure::new(
ValidationFailureLevel::Error,
"Component must have either 'image' or 'application' property".into(),
));
}
// This is a problem because of our left-folding config implementation. A shared application
// could specify additional config and actually overwrite the original manifest's config.
(None, Some(shared_properties)) if !config.is_empty() => {
failures.push(ValidationFailure::new(
ValidationFailureLevel::Error,
format!(
"Shared component '{}' cannot specify additional 'config'",
shared_properties.name
),
));
}
Comment on lines +637 to +647
Copy link
Member

@joonas joonas Sep 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related, would specifying secrets here be a problem? Or perhaps not a problem, but create unnecessary work for the host when it won't actually be passed to the shared component?

// Shared application components already have scale properties defined in their original manifest
(None, Some(shared_properties))
if component
.traits
.as_ref()
.is_some_and(|traits| traits.iter().any(|trt| trt.is_scaler())) =>
{
failures.push(ValidationFailure::new(
ValidationFailureLevel::Error,
format!(
"Shared component '{}' cannot include a scaler trait",
shared_properties.name
),
));
}
_ => {}
},
}
}
failures
}

/// This function validates that a key/value pair is a valid OAM label. It's using fairly
/// basic validation rules to ensure that the manifest isn't doing anything horribly wrong. Keeping
/// this function free of regex is intentional to keep this code functional but simple.
Expand Down
4 changes: 2 additions & 2 deletions crates/wadm-types/wit/deps.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[wadm]
path = "../../../wit/wadm"
sha256 = "30b945b53e5dc1220f25da83449571e119cfd4029647a1908e5658d72335424e"
sha512 = "bbd7e5883dc4014ea246a33cf9386b11803cb330854e5691af526971c7131ad358eec9ad8f6dbf0ccd20efe0fedb43a3304f8e9538832d73cce7db09f82f1176"
sha256 = "be9dcb406ac45d69c18c70d962572b9def1f59787246caf27b54f255a817ace7"
sha512 = "c3a3dc09613acff547165627174323096e50abd31998a3514917cc7b2596d86ae04bf4950cbbfac932fc82b2039f435f278fe25eb6f9d23ab7678662e8d528f1"
12 changes: 10 additions & 2 deletions crates/wadm-types/wit/deps/wadm/types.wit
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,17 @@ interface types {

// Properties for a component
record component-properties {
image: string,
image: option<string>,
application: option<shared-application-component-properties>,
id: option<string>,
config: list<config-property>,
secrets: list<secret-property>,
}

// Properties for a capability
record capability-properties {
image: string,
image: option<string>,
application: option<shared-application-component-properties>,
id: option<string>,
config: list<config-property>,
secrets: list<secret-property>,
Expand Down Expand Up @@ -187,6 +189,12 @@ interface types {
version: option<string>,
}

// Shared application component properties
record shared-application-component-properties {
name: string,
component: string
}

// Target configuration
record target-config {
name: string,
Expand Down
Loading