├── charts └── wadm │ ├── templates │ ├── NOTES.txt │ ├── _helpers.tpl │ └── deployment.yaml │ ├── ct.yaml │ ├── ci │ └── nats.yaml │ ├── .helmignore │ ├── Chart.yaml │ └── values.yaml ├── crates ├── wadm-types │ ├── wit │ │ ├── deps.toml │ │ ├── interfaces.wit │ │ ├── deps.lock │ │ └── deps │ │ │ └── wadm │ │ │ ├── client.wit │ │ │ └── types.wit │ └── Cargo.toml ├── wadm │ ├── src │ │ ├── events │ │ │ ├── mod.rs │ │ │ ├── ser.rs │ │ │ ├── deser.rs │ │ │ └── data.rs │ │ ├── workers │ │ │ ├── mod.rs │ │ │ └── command.rs │ │ ├── scaler │ │ │ └── statusscaler.rs │ │ ├── server │ │ │ ├── parser.rs │ │ │ └── notifier.rs │ │ ├── publisher.rs │ │ ├── connections.rs │ │ ├── test_util.rs │ │ ├── consumers │ │ │ ├── mod.rs │ │ │ └── commands.rs │ │ ├── observer.rs │ │ └── storage │ │ │ └── snapshot.rs │ └── Cargo.toml └── wadm-client │ ├── Cargo.toml │ └── src │ ├── error.rs │ ├── loader.rs │ ├── nats.rs │ └── topics.rs ├── .github ├── CODEOWNERS ├── release.yml ├── dependabot.yml ├── stale.yml ├── actions │ └── configure-wkg │ │ └── action.yml └── workflows │ ├── wit-wadm.yaml │ ├── e2e.yml │ ├── test.yml │ ├── scorecard.yml │ └── chart.yml ├── static └── images │ ├── wadm.png │ ├── wadm_128.png │ └── wadm_256.png ├── rust-toolchain.toml ├── .devcontainer ├── install.sh ├── README.md └── devcontainer.json ├── .gitignore ├── tests ├── nats │ ├── nats-leaf-a.conf │ ├── nats-leaf-b.conf │ ├── nats-leaf-wadm.conf │ └── nats-test.conf ├── fixtures │ ├── nats.jwt │ └── manifests │ │ ├── shared │ │ ├── no_properties.yaml │ │ ├── both_properties.yaml │ │ ├── notshared_http.yaml │ │ ├── shared_component.yaml │ │ ├── shared_http.yaml │ │ ├── no_matching_app.yaml │ │ ├── notshared_http_dev.yaml │ │ ├── no_matching_component.yaml │ │ ├── shared_http_dev.yaml │ │ └── shared_component_dev.yaml │ │ ├── upgradedapp3.yaml │ │ ├── made-up-key.wadm.yaml │ │ ├── host_stop.yaml │ │ ├── policy.wadm.yaml │ │ ├── dangling-link.wadm.yaml │ │ ├── long_image_refs.yaml │ │ ├── custom-interface.wadm.yaml │ │ ├── simple.wadm.yaml │ │ ├── misnamed-interface.wadm.yaml │ │ ├── duplicate_id2.yaml │ │ ├── correct_unique_interface_links.yaml │ │ ├── unknown-package.wadm.yaml │ │ ├── missing_capability_component.yaml │ │ ├── lotta_components.yaml │ │ ├── simple.yaml │ │ ├── incorrect_unique_interface_links.yaml │ │ ├── duplicate_id1.yaml │ │ ├── duplicate_links.yaml │ │ ├── simple2.yaml │ │ ├── all_hosts.yaml │ │ ├── deprecated-source-and-target-config.yaml │ │ ├── upgradedapp2.yaml │ │ ├── upgradedapp.yaml │ │ ├── outdatedapp.yaml │ │ ├── complex.yaml │ │ ├── duplicate_link_config_names.wadm.yaml │ │ ├── incorrect_component.yaml │ │ └── duplicate_component.yaml ├── docker-compose-e2e_upgrades.yaml ├── docker-compose-e2e_shared.yaml ├── docker-compose-e2e_multiple_hosts.yaml ├── docker-compose-e2e_multitenant.yaml └── validation.rs ├── SECURITY.md ├── .envrc ├── src ├── schema.rs ├── main.rs └── logging.rs ├── Dockerfile.wolfi ├── MAINTAINERS.md ├── Dockerfile ├── oam ├── config.yaml ├── echo.yaml ├── custom.yaml ├── hello.yaml ├── simple1.yaml ├── simple2.yaml ├── kvcounter.yaml ├── sqldbpostgres.yaml ├── simple1.json └── README.md ├── wit └── wadm │ ├── client.wit │ └── types.wit ├── Cargo.toml └── Makefile /charts/wadm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /crates/wadm-types/wit/deps.toml: -------------------------------------------------------------------------------- 1 | wadm = "../../../wit/wadm" -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # wasmCloud wadm maintainers 2 | * @wasmCloud/wadm-maintainers 3 | -------------------------------------------------------------------------------- /static/images/wadm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wadm/HEAD/static/images/wadm.png -------------------------------------------------------------------------------- /static/images/wadm_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wadm/HEAD/static/images/wadm_128.png -------------------------------------------------------------------------------- /static/images/wadm_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wasmCloud/wadm/HEAD/static/images/wadm_256.png -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["clippy", "rust-src", "rustfmt"] 4 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # .github/release.yml 2 | 3 | changelog: 4 | exclude: 5 | authors: 6 | - dependabot 7 | -------------------------------------------------------------------------------- /crates/wadm/src/events/mod.rs: -------------------------------------------------------------------------------- 1 | mod data; 2 | mod deser; 3 | mod ser; 4 | mod types; 5 | 6 | pub use data::*; 7 | pub use types::*; 8 | -------------------------------------------------------------------------------- /charts/wadm/ct.yaml: -------------------------------------------------------------------------------- 1 | validate-maintainers: false 2 | target-branch: main # TODO: Remove this once chart-testing 3.10.1+ is released 3 | helm-extra-args: --timeout 60s -------------------------------------------------------------------------------- /charts/wadm/ci/nats.yaml: -------------------------------------------------------------------------------- 1 | config: 2 | jetstream: 3 | enabled: true 4 | fileStore: 5 | pvc: 6 | enabled: false 7 | merge: 8 | domain: default -------------------------------------------------------------------------------- /.devcontainer/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # INSTALL WASH 4 | curl -s https://packagecloud.io/install/repositories/wasmcloud/core/script.deb.sh | sudo bash 5 | sudo apt install wash openssl -y -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | tests/e2e_log/ 3 | 4 | *.dump 5 | 6 | # Thanks MacOS 7 | .DS_Store 8 | 9 | # Ignore IDE specific files 10 | .idea/ 11 | .vscode/ 12 | 13 | .direnv/ 14 | result 15 | -------------------------------------------------------------------------------- /tests/nats/nats-leaf-a.conf: -------------------------------------------------------------------------------- 1 | jetstream { 2 | domain: leaf 3 | } 4 | 5 | leafnodes { 6 | remotes = [ 7 | { 8 | urls: [ 9 | "nats://a:a@nats-core:7422", 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/nats/nats-leaf-b.conf: -------------------------------------------------------------------------------- 1 | jetstream { 2 | domain: leaf 3 | } 4 | 5 | leafnodes { 6 | remotes = [ 7 | { 8 | urls: [ 9 | "nats://b:b@nats-core:7422", 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/fixtures/nats.jwt: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdHJpbmciOiAiQWNjb3JkIHRvIGFsbCBrbm93biBsb3dzIG9mIGF2aWF0aW9uLCB0aGVyZSBpcyBubyB3YXkgdGhhdCBhIGJlZSBhYmxlIHRvIGZseSJ9.GyU6pTRhflcOg6KBCU6wZedP8BQzLXbdgYIoU6KzzD8 -------------------------------------------------------------------------------- /tests/nats/nats-leaf-wadm.conf: -------------------------------------------------------------------------------- 1 | jetstream { 2 | domain: leaf 3 | } 4 | 5 | leafnodes { 6 | remotes = [ 7 | { 8 | urls: [ 9 | "nats://wadm:wadm@nats-core:7422", 10 | ] 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a security issue 2 | 3 | Please refer to the [wasmCloud Security Process and Policy](https://github.com/wasmCloud/wasmCloud/blob/main/SECURITY.md) for details on how to report security issues and vulnerabilities. 4 | -------------------------------------------------------------------------------- /crates/wadm-types/wit/interfaces.wit: -------------------------------------------------------------------------------- 1 | package wasmcloud:wadm-types@0.2.0; 2 | 3 | world interfaces { 4 | import wasmcloud:wadm/types@0.2.0; 5 | import wasmcloud:wadm/client@0.2.0; 6 | import wasmcloud:wadm/handler@0.2.0; 7 | } 8 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then 2 | source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM=" 3 | fi 4 | watch_file rust-toolchain.toml 5 | use flake 6 | -------------------------------------------------------------------------------- /src/schema.rs: -------------------------------------------------------------------------------- 1 | use schemars::schema_for; 2 | use wadm_types::Manifest; 3 | 4 | fn main() { 5 | let schema = schema_for!(Manifest); 6 | let sch = serde_json::to_string_pretty(&schema).unwrap(); 7 | 8 | std::fs::write("oam.schema.json", sch).unwrap(); 9 | } 10 | -------------------------------------------------------------------------------- /crates/wadm-types/wit/deps.lock: -------------------------------------------------------------------------------- 1 | [wadm] 2 | path = "../../../wit/wadm" 3 | sha256 = "9795ab1a83023da07da2dc28d930004bd913b9dbf07d68d9ef9207a44348a169" 4 | sha512 = "9a94f33fd861912c81efd441cd19cc8066dbb2df5c2236d0472b66294bddc20ec5ad569484be18334d8c104ae9647b2c81c9878210ac35694ad8ba4a5b3780be" 5 | -------------------------------------------------------------------------------- /Dockerfile.wolfi: -------------------------------------------------------------------------------- 1 | FROM cgr.dev/chainguard/wolfi-base:latest AS base 2 | 3 | FROM base AS base-amd64 4 | ARG BIN_AMD64 5 | ARG BIN=$BIN_AMD64 6 | 7 | FROM base AS base-arm64 8 | ARG BIN_ARM64 9 | ARG BIN=$BIN_ARM64 10 | 11 | FROM base-$TARGETARCH 12 | 13 | # Copy application binary from disk 14 | COPY ${BIN} /usr/local/bin/wadm 15 | 16 | # Run the application 17 | ENTRYPOINT ["/usr/local/bin/wadm"] 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | day: "monday" 8 | time: "09:00" 9 | timezone: "America/New_York" 10 | - package-ecosystem: "github-actions" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | day: "monday" 15 | time: "09:00" 16 | timezone: "America/New_York" -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/no_properties.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: no-props 5 | annotations: 6 | description: 'Contains a component with neither image and application' 7 | spec: 8 | components: 9 | - name: httpserver 10 | type: capability 11 | properties: 12 | config: 13 | - name: log 14 | properties: 15 | level: info 16 | -------------------------------------------------------------------------------- /crates/wadm/src/workers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Various [`Worker`](crate::consumers::manager::Worker) implementations for reconciling and 2 | //! handling events and commands. These are essentially the default things that drive work forward 3 | //! in wadm 4 | 5 | mod command; 6 | mod event; 7 | mod event_helpers; 8 | 9 | pub use command::CommandWorker; 10 | pub(crate) use event::get_commands_and_result; 11 | pub use event::EventWorker; 12 | pub use event_helpers::*; 13 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/both_properties.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: both-props 5 | annotations: 6 | description: 'Contains a component with image and application' 7 | spec: 8 | components: 9 | - name: httpserver 10 | type: capability 11 | properties: 12 | image: pull-from-me 13 | application: 14 | name: wheee 15 | component: httpserver 16 | -------------------------------------------------------------------------------- /crates/wadm/src/events/ser.rs: -------------------------------------------------------------------------------- 1 | //! Custom serializers for event fields 2 | 3 | use serde::Serializer; 4 | 5 | pub(crate) fn tags(data: &Option>, serializer: S) -> Result 6 | where 7 | S: Serializer, 8 | { 9 | match data { 10 | Some(v) => { 11 | let joined = v.join(","); 12 | serializer.serialize_some(&joined) 13 | } 14 | None => serializer.serialize_none(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/upgradedapp3.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: dontupdateapp 5 | annotations: 6 | description: 'Testing effect of duplicate providers' 7 | spec: 8 | components: 9 | - name: httpserver-other 10 | type: capability 11 | properties: 12 | image: ghcr.io/wasmcloud/http-server:0.23.0 13 | # This ID should not be allowed to be deployed 14 | id: http_server 15 | -------------------------------------------------------------------------------- /charts/wadm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | ci/ 25 | .helmignore -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # wadm devcontainer 2 | 3 | A simple devcontainer that has `rust` installed. Try it out with `devcontainer open` at the root of this repository! 4 | 5 | As a `postCreateCommand`, we run an install script that grabs an alpha released version of `wadm` for you and installs a compatible (currently unreleased) version of `wash` for you to get started with. 6 | 7 | ## Prerequisites 8 | 9 | - [devcontainer CLI](https://code.visualstudio.com/docs/devcontainers/devcontainer-cli#_installation) 10 | - VSCode 11 | -------------------------------------------------------------------------------- /crates/wadm/src/events/deser.rs: -------------------------------------------------------------------------------- 1 | //! Custom deserializers for event fields 2 | 3 | use serde::{Deserialize, Deserializer}; 4 | 5 | pub(crate) fn tags<'de, D>(deserializer: D) -> Result>, D::Error> 6 | where 7 | D: Deserializer<'de>, 8 | { 9 | let s: Option = Deserialize::deserialize(deserializer)?; 10 | // NOTE: We could probably do something to avoid extra allocations here, but not worth it at 11 | // this time 12 | Ok(s.map(|st| st.split(',').map(ToOwned::to_owned).collect())) 13 | } 14 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/notshared_http.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: not-shared-http 5 | annotations: 6 | description: 'My Precious! O my Precious! We needs it. Must have the precious. They stole it from us' 7 | spec: 8 | components: 9 | - name: httpserver 10 | type: capability 11 | properties: 12 | image: ghcr.io/wasmcloud/http-server:0.23.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 1 17 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/made-up-key.wadm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: sample 6 | annotations: 7 | version: v0.0.1 8 | description: Sample manifest that passes 9 | spec: 10 | components: 11 | - name: http-component 12 | type: component 13 | properties: 14 | image: ghcr.io/wasmcloud/component-http-hello-world:0.1.0 15 | traits: 16 | - type: spreadscaler 17 | properties: 18 | replicas: 1 19 | incorrect: true 20 | -------------------------------------------------------------------------------- /tests/docker-compose-e2e_upgrades.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | nats: 3 | image: nats:2.10-alpine 4 | command: ['-js'] 5 | ports: 6 | - 4222:4222 7 | wasmcloud: 8 | image: wasmcloud/wasmcloud:latest 9 | depends_on: 10 | - nats 11 | deploy: 12 | replicas: 1 13 | environment: 14 | LC_ALL: en_US.UTF-8 15 | RUST_LOG: debug,hyper=info 16 | WASMCLOUD_NATS_HOST: nats 17 | WASMCLOUD_CLUSTER_SEED: SCAOGJWX53TGI4233T6GAXWYWBIB5ZDGPTCO6ODJQYELS52YCQCBQSRPA4 18 | HOST_app: upgradey 19 | HOST_region: us-brooks-east 20 | HOST_high_availability: nope 21 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # MAINTAINERS 2 | 3 | The following individuals are responsible for reviewing code, managing issues, and ensuring the overall quality of `wadm`. 4 | 5 | ## @wasmCloud/wadm-maintainers 6 | 7 | Name: Joonas Bergius 8 | GitHub: @joonas 9 | Organization: Cosmonic 10 | 11 | Name: Dan Norris 12 | GitHub: @protochron 13 | Organization: Cosmonic 14 | 15 | Name: Taylor Thomas 16 | GitHub: @thomastaylor312 17 | Organization: Cosmonic 18 | 19 | Name: Ahmed Tadde 20 | GitHub: @ahmedtadde 21 | Organization: PreciseTarget 22 | 23 | Name: Brooks Townsend 24 | GitHub: @brooksmtownsend 25 | Organization: Cosmonic 26 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/host_stop.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: host-stop 5 | annotations: 6 | description: 'This is my app' 7 | spec: 8 | components: 9 | - name: hello 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 5 17 | spread: 18 | - name: eastcoast 19 | requirements: 20 | region: us-brooks-east 21 | weight: 100 22 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/shared_component.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: shared-component 5 | annotations: 6 | description: 'A shared component!' 7 | experimental.wasmcloud.dev/shared: 'true' 8 | spec: 9 | components: 10 | - name: link-to-meee 11 | type: component 12 | properties: 13 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 14 | config: 15 | - name: defaults 16 | properties: 17 | left: right 18 | traits: 19 | - type: spreadscaler 20 | properties: 21 | instances: 1 22 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/policy.wadm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: sample 6 | annotations: 7 | version: v0.0.1 8 | description: Sample manifest that passes 9 | spec: 10 | policies: 11 | - name: secrets-policy 12 | type: policy.secret.wasmcloud.dev/v1alpha1 13 | properties: 14 | metadata: a-value 15 | components: 16 | - name: http-component 17 | type: component 18 | properties: 19 | image: ghcr.io/wasmcloud/component-http-hello-world:0.1.0 20 | traits: 21 | - type: spreadscaler 22 | properties: 23 | replicas: 1 24 | -------------------------------------------------------------------------------- /crates/wadm-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wadm-client" 3 | description = "A client library for interacting with the wadm API" 4 | version = "0.10.0" 5 | edition = "2021" 6 | authors = ["wasmCloud Team"] 7 | keywords = ["webassembly", "wasmcloud", "wadm"] 8 | license = "Apache-2.0" 9 | repository = "https://github.com/wasmcloud/wadm" 10 | 11 | [dependencies] 12 | anyhow = { workspace = true } 13 | async-nats = { workspace = true } 14 | futures = { workspace = true } 15 | nkeys = { workspace = true } 16 | serde_json = { workspace = true } 17 | serde_yaml = { workspace = true } 18 | thiserror = { workspace = true } 19 | tokio = { workspace = true, features = ["full"] } 20 | wadm-types = { workspace = true } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye-slim AS base 2 | 3 | RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y ca-certificates 4 | 5 | FROM base AS base-amd64 6 | ARG BIN_AMD64 7 | ARG BIN=$BIN_AMD64 8 | 9 | FROM base AS base-arm64 10 | ARG BIN_ARM64 11 | ARG BIN=$BIN_ARM64 12 | 13 | FROM base-$TARGETARCH 14 | 15 | ARG USERNAME=wadm 16 | ARG USER_UID=1000 17 | ARG USER_GID=$USER_UID 18 | 19 | RUN addgroup --gid $USER_GID $USERNAME \ 20 | && adduser --disabled-login -u $USER_UID --ingroup $USERNAME $USERNAME 21 | 22 | # Copy application binary from disk 23 | COPY --chown=$USERNAME ${BIN} /usr/local/bin/wadm 24 | 25 | USER $USERNAME 26 | # Run the application 27 | ENTRYPOINT ["/usr/local/bin/wadm"] 28 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/dangling-link.wadm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: dangling-link 6 | annotations: 7 | version: v0.0.1 8 | description: Manifest with a dangling link 9 | spec: 10 | components: 11 | - name: http-component 12 | type: component 13 | properties: 14 | image: ghcr.io/wasmcloud/component-http-hello-world:0.1.0 15 | traits: 16 | - type: spreadscaler 17 | properties: 18 | instances: 1 19 | - type: link 20 | properties: 21 | namespace: wasi 22 | package: http 23 | interfaces: [outgoing-handler] 24 | target: 25 | name: httpclient 26 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wadm", 3 | "image": "mcr.microsoft.com/devcontainers/rust:latest", 4 | "features": { 5 | "ghcr.io/devcontainers/features/common-utils:2": {} 6 | }, 7 | "containerEnv": { 8 | "RUST_LOG": "INFO" 9 | }, 10 | "customizations": { 11 | "vscode": { 12 | "settings": { 13 | "files.watcherExclude": { 14 | "**/target/**": true 15 | }, 16 | "[rust]": { 17 | "editor.formatOnSave": true 18 | } 19 | }, 20 | "extensions": [ 21 | "rust-lang.rust-analyzer", 22 | "tamasfe.even-better-toml" 23 | ] 24 | } 25 | }, 26 | "postCreateCommand": "bash ./.devcontainer/install.sh", 27 | "workspaceMount": "source=${localWorkspaceFolder},target=/wadm,type=bind,consistency=cached", 28 | "workspaceFolder": "/wadm" 29 | } 30 | -------------------------------------------------------------------------------- /tests/docker-compose-e2e_shared.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | nats: 3 | image: nats:2.10-alpine 4 | command: ['-js'] 5 | ports: 6 | - 4222:4222 7 | wasmcloud_test_host_one: 8 | image: wasmcloud/wasmcloud:latest 9 | depends_on: 10 | - nats 11 | deploy: 12 | replicas: 2 13 | environment: 14 | LC_ALL: en_US.UTF-8 15 | RUST_LOG: debug,hyper=info 16 | WASMCLOUD_NATS_HOST: nats 17 | WASMCLOUD_LATTICE: shared_providers 18 | wasmcloud_test_host_two: 19 | image: wasmcloud/wasmcloud:latest 20 | depends_on: 21 | - nats 22 | deploy: 23 | replicas: 2 24 | environment: 25 | LC_ALL: en_US.UTF-8 26 | RUST_LOG: debug,hyper=info 27 | WASMCLOUD_NATS_HOST: nats 28 | WASMCLOUD_LATTICE: shared_components 29 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/long_image_refs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: echo-simple 5 | annotations: 6 | description: "This is my app" 7 | spec: 8 | components: 9 | - name: echo 10 | type: component 11 | properties: 12 | image: file:///some/path/to/another/path/that/is/very/long/and/would/normally/crash/the/thing/but/in/this/case/doesnt/because/the/size/is/changed.wasm 13 | - name: httpserver 14 | type: capability 15 | properties: 16 | image: file:///some/path/to/another/path/that/is/very/long/and/would/normally/crash/the/thing/but/in/this/case/doesnt/because/the/size/is/changed.exe 17 | traits: 18 | - type: spreadscaler 19 | properties: 20 | instances: 1 21 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/shared_http.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: shared-http 5 | annotations: 6 | description: 'A shared HTTP server and client, for everybody!!!!!!!!!!!!!!!!!!!!' 7 | experimental.wasmcloud.dev/shared: 'true' 8 | spec: 9 | components: 10 | - name: httpclient 11 | type: capability 12 | properties: 13 | image: ghcr.io/wasmcloud/http-client:0.12.0 14 | traits: 15 | - type: spreadscaler 16 | properties: 17 | instances: 1 18 | - name: httpserver 19 | type: capability 20 | properties: 21 | image: ghcr.io/wasmcloud/http-server:0.23.0 22 | traits: 23 | - type: spreadscaler 24 | properties: 25 | instances: 1 26 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/custom-interface.wadm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: custom-interface 6 | annotations: 7 | version: v0.0.1 8 | description: A component with a completely custom interface 9 | spec: 10 | components: 11 | - name: counter 12 | type: component 13 | properties: 14 | image: ghcr.io/wasmcloud/component-http-keyvalue-counter:0.1.0 15 | traits: 16 | - type: spreadscaler 17 | properties: 18 | instances: 1 19 | - type: link 20 | properties: 21 | target: 22 | name: kvredis 23 | namespace: my-wasi 24 | package: my-keyvalue 25 | interfaces: [my-atomics] 26 | 27 | - name: kvredis 28 | type: capability 29 | properties: 30 | image: ghcr.io/wasmcloud/keyvalue-redis:0.24.0 31 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/simple.wadm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: sample 6 | annotations: 7 | version: v0.0.1 8 | description: Sample manifest that passes 9 | spec: 10 | policies: 11 | - name: vault-for-component 12 | type: policy.secret.wasmcloud.dev/v1alpha1 13 | properties: 14 | backend: 'vault-prod' 15 | role_name: 'test' 16 | mount_path: 'jwt' 17 | 18 | components: 19 | - name: http-component 20 | type: component 21 | properties: 22 | image: ghcr.io/wasmcloud/component-http-hello-world:0.1.0 23 | secrets: 24 | - name: test-secret 25 | properties: 26 | policy: 'vault-for-component' 27 | key: 'secret/test' 28 | traits: 29 | - type: spreadscaler 30 | properties: 31 | instances: 1 32 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/misnamed-interface.wadm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: misnamed-interface 6 | annotations: 7 | version: v0.0.1 8 | description: A component with a misnamed interface 9 | spec: 10 | components: 11 | - name: counter 12 | type: component 13 | properties: 14 | image: ghcr.io/wasmcloud/component-http-keyvalue-counter:0.1.0 15 | traits: 16 | - type: spreadscaler 17 | properties: 18 | instances: 1 19 | - type: link 20 | properties: 21 | namespace: wasi 22 | package: keyvalue 23 | interfaces: [atomic] # BUG: should be 'atomics' 24 | target: 25 | name: kvredis 26 | 27 | - name: kvredis 28 | type: capability 29 | properties: 30 | image: ghcr.io/wasmcloud/keyvalue-redis:0.24.0 31 | -------------------------------------------------------------------------------- /crates/wadm-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wadm-types" 3 | description = "Types and validators for the wadm API" 4 | version = "0.8.3" 5 | edition = "2021" 6 | authors = ["wasmCloud Team"] 7 | keywords = ["webassembly", "wasmcloud", "wadm"] 8 | license = "Apache-2.0" 9 | repository = "https://github.com/wasmcloud/wadm" 10 | 11 | [features] 12 | wit = [] 13 | 14 | [dependencies] 15 | anyhow = { workspace = true } 16 | regex = { workspace = true } 17 | schemars = { workspace = true } 18 | serde = { workspace = true, features = ["derive"] } 19 | serde_json = { workspace = true } 20 | serde_yaml = { workspace = true } 21 | utoipa = { workspace = true } 22 | 23 | [target.'cfg(not(target_family = "wasm"))'.dependencies] 24 | tokio = { workspace = true, features = ["full"] } 25 | wit-bindgen-wrpc = { workspace = true } 26 | 27 | [target.'cfg(target_family = "wasm")'.dependencies] 28 | wit-bindgen = { workspace = true, features = ["macros"] } 29 | -------------------------------------------------------------------------------- /oam/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: config-example 5 | annotations: 6 | description: 'This is my app' 7 | spec: 8 | components: 9 | - name: http 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | # You can pass any config data you'd like sent to your component as a string->string map 14 | config: 15 | - name: component_config 16 | properties: 17 | lang: EN-US 18 | 19 | - name: webcap 20 | type: capability 21 | properties: 22 | image: ghcr.io/wasmcloud/http-server:0.23.0 23 | # You can pass any config data you'd like sent to your provider as a string->string map 24 | config: 25 | - name: provider_config 26 | properties: 27 | default-port: '8080' 28 | cache_file: '/tmp/mycache.json' 29 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/duplicate_id2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: petclinic 5 | annotations: 6 | description: "wasmCloud Pet Clinic Sample" 7 | spec: 8 | components: 9 | - name: ui 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/ui:0.3.2 13 | id: ui 14 | traits: 15 | - type: spreadscaler 16 | properties: 17 | instances: 1 18 | spread: 19 | - name: uiclinicapp 20 | requirements: 21 | app: petclinic 22 | 23 | - name: ui2 24 | type: component 25 | properties: 26 | image: wasmcloud.azurecr.io/ui:0.3.2 27 | id: ui 28 | traits: 29 | - type: spreadscaler 30 | properties: 31 | instances: 1 32 | spread: 33 | - name: customersclinicapp 34 | requirements: 35 | app: petclinic 36 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned # Pinned issues should stick around 8 | - security # Security issues need to be resolved 9 | - roadmap # Issue is captured on the wasmCloud roadmap and won't be lost 10 | # Label to use when marking an issue as stale 11 | staleLabel: stale 12 | # Comment to post when marking an issue as stale. Set to `false` to disable 13 | markComment: > 14 | This issue has been automatically marked as stale because it has not had 15 | recent activity. It will be closed if no further activity occurs. If this 16 | has been closed too eagerly, please feel free to tag a maintainer so we can 17 | keep working on the issue. Thank you for contributing to wasmCloud! 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false 20 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/correct_unique_interface_links.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: test-different-interfaces 5 | annotations: 6 | description: "test" 7 | spec: 8 | components: 9 | - name: my-component 10 | type: component 11 | properties: 12 | image: test:latest 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 1 17 | - type: link 18 | properties: 19 | target: redis 20 | namespace: wasi 21 | package: keyvalue 22 | interfaces: [atomics] 23 | - type: link 24 | properties: 25 | target: redis 26 | namespace: wasi 27 | package: keyvalue 28 | interfaces: [store] 29 | - name: redis 30 | type: capability 31 | properties: 32 | image: test:latest 33 | traits: 34 | - type: spreadscaler 35 | properties: 36 | instances: 1 37 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/unknown-package.wadm.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: unknown-package 6 | annotations: 7 | version: v0.0.1 8 | description: A component with an unknown package 9 | spec: 10 | components: 11 | - name: counter 12 | type: component 13 | properties: 14 | image: ghcr.io/wasmcloud/component-http-keyvalue-counter:0.1.0 15 | traits: 16 | - type: spreadscaler 17 | properties: 18 | instances: 1 19 | - type: link 20 | properties: 21 | target: 22 | name: kvredis 23 | config: 24 | - name: test 25 | properties: 26 | test: value 27 | namespace: wasi 28 | package: keyvalues # BUG: should be 'keyvalue' 29 | interfaces: [atomics] 30 | 31 | - name: kvredis 32 | type: capability 33 | properties: 34 | image: ghcr.io/wasmcloud/keyvalue-redis:0.24.0 35 | -------------------------------------------------------------------------------- /oam/echo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: echo 5 | annotations: 6 | description: 'This is my app' 7 | spec: 8 | components: 9 | - name: echo 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/echo:0.3.7 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 1 17 | 18 | - name: httpserver 19 | type: capability 20 | properties: 21 | image: wasmcloud.azurecr.io/httpserver:0.17.0 22 | traits: 23 | - type: spreadscaler 24 | properties: 25 | instances: 1 26 | - type: link 27 | properties: 28 | target: 29 | name: echo 30 | namespace: wasi 31 | package: http 32 | interfaces: 33 | - incoming-handler 34 | source: 35 | config: 36 | - name: default-port 37 | properties: 38 | address: 0.0.0.0:8080 39 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/missing_capability_component.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: echo-simple 5 | annotations: 6 | description: "This is my app" 7 | spec: 8 | components: 9 | - name: echo 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/echo:0.3.7 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 4 17 | - type: linkdef 18 | properties: 19 | # This is a simple typo which should be caught by validation: there is no capability component named "httpclyent" 20 | target: 21 | name: httpclyent 22 | namespace: wasi 23 | package: http 24 | interfaces: ["outgoing-handler"] 25 | 26 | - name: httpclient 27 | type: capability 28 | properties: 29 | image: wasmcloud.azurecr.io/httpclient:0.17.0 30 | traits: 31 | - type: spreadscaler 32 | properties: 33 | instances: 1 34 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/no_matching_app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: some-nonexistant-app 5 | annotations: 6 | description: 'Manifest that refers to a nonexistant app' 7 | spec: 8 | components: 9 | - name: hello 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 12 17 | - name: httpserver 18 | type: capability 19 | properties: 20 | application: 21 | name: some-nonexistant-app 22 | component: httpserver 23 | traits: 24 | - type: link 25 | properties: 26 | namespace: wasi 27 | package: http 28 | interfaces: [incoming-handler] 29 | target: 30 | name: hello 31 | source: 32 | config: 33 | - name: httpaddr 34 | properties: 35 | address: 0.0.0.0:8080 36 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/notshared_http_dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: not-shared-http-dev 5 | annotations: 6 | description: 'A Hello World app that tries to use a not shared component' 7 | spec: 8 | components: 9 | - name: hello 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 12 17 | - name: httpserver 18 | type: capability 19 | properties: 20 | application: 21 | name: not-shared-http 22 | component: httpserver 23 | traits: 24 | - type: link 25 | properties: 26 | namespace: wasi 27 | package: http 28 | interfaces: [incoming-handler] 29 | target: 30 | name: hello 31 | source: 32 | config: 33 | - name: httpaddr 34 | properties: 35 | address: 0.0.0.0:8080 36 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/lotta_components.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: lotta-components 5 | annotations: 6 | description: 'This is my, big, app' 7 | spec: 8 | components: 9 | - name: hello 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 9001 17 | 18 | - name: httpserver 19 | type: capability 20 | properties: 21 | image: ghcr.io/wasmcloud/http-server:0.23.0 22 | traits: 23 | - type: spreadscaler 24 | properties: 25 | instances: 1 26 | - type: link 27 | properties: 28 | target: 29 | name: hello 30 | namespace: wasi 31 | package: http 32 | interfaces: [incoming-handler] 33 | source: 34 | config: 35 | - name: httpaddr 36 | properties: 37 | address: 0.0.0.0:8080 38 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/no_matching_component.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: no-matching-component 5 | annotations: 6 | description: 'Manifest that refers to a nonexistant component' 7 | spec: 8 | components: 9 | - name: hello 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 12 17 | - name: httpserver 18 | type: capability 19 | properties: 20 | application: 21 | name: shared-http 22 | component: some-nonexistant-component 23 | traits: 24 | - type: link 25 | properties: 26 | namespace: wasi 27 | package: http 28 | interfaces: [incoming-handler] 29 | target: 30 | name: hello 31 | source: 32 | config: 33 | - name: httpaddr 34 | properties: 35 | address: 0.0.0.0:8080 36 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/simple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: hello-simple 5 | annotations: 6 | description: 'A Hello World app for testing, most basic HTTP link' 7 | spec: 8 | components: 9 | - name: hello 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | id: http_hello_world 14 | traits: 15 | - type: spreadscaler 16 | properties: 17 | instances: 4 18 | 19 | - name: httpserver 20 | type: capability 21 | properties: 22 | image: ghcr.io/wasmcloud/http-server:0.23.0 23 | id: http_server 24 | traits: 25 | - type: spreadscaler 26 | properties: 27 | instances: 1 28 | - type: link 29 | properties: 30 | namespace: wasi 31 | package: http 32 | interfaces: [incoming-handler] 33 | target: 34 | name: hello 35 | source: 36 | config: 37 | - name: httpaddr 38 | properties: 39 | address: 0.0.0.0:8080 40 | -------------------------------------------------------------------------------- /oam/custom.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: my-example-app 5 | annotations: 6 | description: "This is my app revision 2" 7 | spec: 8 | components: 9 | - name: userinfo 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/fake:1 13 | traits: 14 | # NOTE: This demonstrates what a custom scaler could look like. This functionality does not currently exist 15 | - type: customscaler 16 | properties: 17 | instances: 4 18 | clouds: 19 | - aws 20 | - azure 21 | scale_profile: mini 22 | 23 | - name: webcap 24 | type: capability 25 | properties: 26 | image: wasmcloud.azurecr.io/httpserver:0.13.1 27 | traits: 28 | - type: link 29 | properties: 30 | target: 31 | name: userinfo 32 | namespace: wasi 33 | package: http 34 | interfaces: 35 | - incoming-handler 36 | source: 37 | config: 38 | - name: default-port 39 | properties: 40 | port: "8080" 41 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/incorrect_unique_interface_links.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: test-duplicate-interfaces 5 | annotations: 6 | description: "test" 7 | spec: 8 | components: 9 | - name: my-component 10 | type: component 11 | properties: 12 | image: test:latest 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 1 17 | - type: link 18 | properties: 19 | target: redis-1 20 | namespace: wasi 21 | package: keyvalue 22 | interfaces: [atomics] 23 | - type: link 24 | properties: 25 | target: redis-2 26 | namespace: wasi 27 | package: keyvalue 28 | interfaces: [atomics] 29 | - name: redis-1 30 | type: capability 31 | properties: 32 | image: test:latest 33 | traits: 34 | - type: spreadscaler 35 | properties: 36 | instances: 1 37 | - name: redis-2 38 | type: capability 39 | properties: 40 | image: test:latest 41 | traits: 42 | - type: spreadscaler 43 | properties: 44 | instances: 1 45 | -------------------------------------------------------------------------------- /charts/wadm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: wadm 3 | description: A Helm chart for deploying wadm on Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: '0.2.10' 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: 'v0.21.0' 25 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use clap::Parser; 3 | use wadm::{config::WadmConfig, start_wadm}; 4 | 5 | mod logging; 6 | 7 | #[tokio::main] 8 | async fn main() -> anyhow::Result<()> { 9 | let args = WadmConfig::parse(); 10 | 11 | logging::configure_tracing( 12 | args.structured_logging, 13 | args.tracing_enabled, 14 | args.tracing_endpoint.clone(), 15 | ); 16 | 17 | let mut wadm = start_wadm(args).await.context("failed to run wadm")?; 18 | tokio::select! { 19 | res = wadm.join_next() => { 20 | match res { 21 | Some(Ok(_)) => { 22 | tracing::info!("WADM has exited successfully"); 23 | std::process::exit(0); 24 | } 25 | Some(Err(e)) => { 26 | tracing::error!("WADM has exited with an error: {:?}", e); 27 | std::process::exit(1); 28 | } 29 | None => { 30 | tracing::info!("WADM server did not start"); 31 | std::process::exit(0); 32 | } 33 | } 34 | } 35 | _ = tokio::signal::ctrl_c() => { 36 | tracing::info!("Received Ctrl+C, shutting down"); 37 | std::process::exit(0); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /oam/hello.yaml: -------------------------------------------------------------------------------- 1 | # Metadata 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: hello-world 6 | annotations: 7 | description: 'HTTP hello world demo' 8 | spec: 9 | components: 10 | - name: http-component 11 | type: component 12 | properties: 13 | # Run components from OCI registries as below or from a local .wasm component binary. 14 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 15 | traits: 16 | # One replica of this component will run 17 | - type: spreadscaler 18 | properties: 19 | instances: 1 20 | # The httpserver capability provider, started from the official wasmCloud OCI artifact 21 | - name: httpserver 22 | type: capability 23 | properties: 24 | image: ghcr.io/wasmcloud/http-server:0.23.0 25 | traits: 26 | # Link the HTTP server and set it to listen on the local machine's port 8080 27 | - type: link 28 | properties: 29 | target: 30 | name: http-component 31 | namespace: wasi 32 | package: http 33 | interfaces: [incoming-handler] 34 | source: 35 | config: 36 | - name: default-http 37 | properties: 38 | ADDRESS: 127.0.0.1:8080 39 | -------------------------------------------------------------------------------- /tests/docker-compose-e2e_multiple_hosts.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | nats: 3 | image: nats:2.10-alpine 4 | command: ['-js'] 5 | ports: 6 | - 4222:4222 7 | # Have hosts in 3 different "regions" 8 | wasmcloud_east: 9 | image: wasmcloud/wasmcloud:latest 10 | depends_on: 11 | - nats 12 | deploy: 13 | replicas: 2 14 | environment: 15 | LC_ALL: en_US.UTF-8 16 | RUST_LOG: debug,hyper=info 17 | WASMCLOUD_NATS_HOST: nats 18 | WASMCLOUD_CLUSTER_SEED: SCAOGJWX53TGI4233T6GAXWYWBIB5ZDGPTCO6ODJQYELS52YCQCBQSRPA4 19 | WASMCLOUD_LABEL_region: us-brooks-east 20 | wasmcloud_west: 21 | image: wasmcloud/wasmcloud:latest 22 | depends_on: 23 | - nats 24 | deploy: 25 | replicas: 2 26 | environment: 27 | LC_ALL: en_US.UTF-8 28 | RUST_LOG: debug,hyper=info 29 | WASMCLOUD_NATS_HOST: nats 30 | WASMCLOUD_CLUSTER_SEED: SCAOGJWX53TGI4233T6GAXWYWBIB5ZDGPTCO6ODJQYELS52YCQCBQSRPA4 31 | WASMCLOUD_LABEL_region: us-taylor-west 32 | wasmcloud_moon: 33 | image: wasmcloud/wasmcloud:latest 34 | depends_on: 35 | - nats 36 | deploy: 37 | replicas: 1 38 | environment: 39 | LC_ALL: en_US.UTF-8 40 | RUST_LOG: debug,hyper=info 41 | WASMCLOUD_NATS_HOST: nats 42 | WASMCLOUD_CLUSTER_SEED: SCAOGJWX53TGI4233T6GAXWYWBIB5ZDGPTCO6ODJQYELS52YCQCBQSRPA4 43 | WASMCLOUD_LABEL_region: moon 44 | -------------------------------------------------------------------------------- /.github/actions/configure-wkg/action.yml: -------------------------------------------------------------------------------- 1 | name: Install and configure wkg (linux only) 2 | 3 | inputs: 4 | wkg-version: 5 | description: version of wkg to install. Should be a valid tag from https://github.com/bytecodealliance/wasm-pkg-tools/releases 6 | default: "v0.6.0" 7 | oci-username: 8 | description: username for oci registry 9 | required: true 10 | oci-password: 11 | description: password for oci registry 12 | required: true 13 | 14 | runs: 15 | using: composite 16 | steps: 17 | - name: Download wkg 18 | shell: bash 19 | run: | 20 | curl --fail -L https://github.com/bytecodealliance/wasm-pkg-tools/releases/download/${{ inputs.wkg-version }}/wkg-x86_64-unknown-linux-gnu -o wkg 21 | chmod +x wkg; 22 | echo "$(realpath .)" >> "$GITHUB_PATH"; 23 | - name: Generate and set wkg config 24 | shell: bash 25 | env: 26 | WKG_OCI_USERNAME: ${{ inputs.oci-username }} 27 | WKG_OCI_PASSWORD: ${{ inputs.oci-password }} 28 | run: | 29 | cat << EOF > wkg-config.toml 30 | [namespace_registries] 31 | wasmcloud = "wasmcloud.com" 32 | wrpc = "bytecodealliance.org" 33 | wasi = "wasi.dev" 34 | 35 | [registry."wasmcloud.com".oci] 36 | auth = { username = "${WKG_OCI_USERNAME}", password = "${WKG_OCI_PASSWORD}" } 37 | EOF 38 | echo "WKG_CONFIG_FILE=$(realpath wkg-config.toml)" >> $GITHUB_ENV 39 | -------------------------------------------------------------------------------- /crates/wadm-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | /// Errors that can occur when interacting with the wadm client. 6 | #[derive(Error, Debug)] 7 | pub enum ClientError { 8 | /// Unable to load the manifest from a given source. The underlying error is anyhow::Error to 9 | /// allow for flexibility in loading from different sources. 10 | #[error("Unable to load manifest: {0:?}")] 11 | ManifestLoad(anyhow::Error), 12 | /// An error occurred with the NATS transport 13 | #[error(transparent)] 14 | NatsError(#[from] async_nats::RequestError), 15 | /// An API error occurred with the request 16 | #[error("Invalid request: {0}")] 17 | ApiError(String), 18 | /// The named model was not found 19 | #[error("Model not found: {0}")] 20 | NotFound(String), 21 | /// Unable to serialize or deserialize YAML or JSON data. 22 | #[error("Unable to parse manifest: {0:?}")] 23 | Serialization(#[from] SerializationError), 24 | /// Any other errors that are not covered by the other error cases 25 | #[error(transparent)] 26 | Other(#[from] anyhow::Error), 27 | } 28 | 29 | /// Errors that can occur when serializing or deserializing YAML or JSON data. 30 | #[derive(Error, Debug)] 31 | pub enum SerializationError { 32 | #[error(transparent)] 33 | Yaml(#[from] serde_yaml::Error), 34 | #[error(transparent)] 35 | Json(#[from] serde_json::Error), 36 | } 37 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/duplicate_id1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: my-example-app 5 | annotations: 6 | description: "This is my app" 7 | spec: 8 | components: 9 | - name: userinfo 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/fake:1 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 4 17 | spread: 18 | - name: eastcoast 19 | requirements: 20 | zone: us-east-1 21 | weight: 80 22 | - name: westcoast 23 | requirements: 24 | zone: us-west-1 25 | weight: 20 26 | 27 | - name: webcap 28 | type: capability 29 | properties: 30 | id: httpserver 31 | image: wasmcloud.azurecr.io/httpserver:0.13.1 32 | traits: 33 | - type: linkdef 34 | properties: 35 | target: 36 | name: userinfo 37 | namespace: wasi 38 | package: http 39 | interfaces: ["incoming-handler"] 40 | source: 41 | config: 42 | - name: default-port 43 | properties: 44 | address: "0.0.0.0:8080" 45 | 46 | - name: webcap2 47 | type: capability 48 | properties: 49 | id: httpserver 50 | image: wasmcloud.azurecr.io/httpserver:0.14.1 51 | -------------------------------------------------------------------------------- /oam/simple1.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: my-example-app 5 | annotations: 6 | description: "This is my app" 7 | spec: 8 | components: 9 | - name: userinfo 10 | type: actor 11 | properties: 12 | image: wasmcloud.azurecr.io/fake:1 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 4 17 | spread: 18 | - name: eastcoast 19 | requirements: 20 | zone: us-east-1 21 | weight: 80 22 | - name: westcoast 23 | requirements: 24 | zone: us-west-1 25 | weight: 20 26 | 27 | - name: webcap 28 | type: capability 29 | properties: 30 | image: wasmcloud.azurecr.io/httpserver:0.13.1 31 | traits: 32 | - type: link 33 | properties: 34 | target: 35 | name: webcap 36 | namespace: wasi 37 | package: http 38 | interfaces: ["incoming-handler"] 39 | name: default 40 | 41 | - name: ledblinky 42 | type: capability 43 | properties: 44 | image: wasmcloud.azurecr.io/ledblinky:0.0.1 45 | traits: 46 | - type: spreadscaler 47 | properties: 48 | instances: 1 49 | spread: 50 | - name: haslights 51 | requirements: 52 | ledenabled: "true" 53 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/duplicate_links.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: test-link-name-uniqueness 5 | annotations: 6 | description: 'test' 7 | spec: 8 | components: 9 | - name: http-component 10 | type: component 11 | properties: 12 | image: file://./build/http_hello_world_s.wasm 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 1 17 | - name: http-component-two 18 | type: component 19 | properties: 20 | image: file://./build/http_hello_world_s.wasm 21 | traits: 22 | - type: spreadscaler 23 | properties: 24 | instances: 1 25 | - name: httpserver 26 | type: capability 27 | properties: 28 | image: ghcr.io/wasmcloud/http-server:0.22.0 29 | traits: 30 | - type: link 31 | properties: 32 | target: http-component 33 | namespace: wasi 34 | package: http 35 | interfaces: [incoming-handler] 36 | source_config: 37 | - name: default-http 38 | properties: 39 | address: 127.0.0.1:8080 40 | - type: link 41 | properties: 42 | target: http-component-two 43 | namespace: wasi 44 | package: http 45 | interfaces: [incoming-handler] 46 | source_config: 47 | - name: default-http-two 48 | properties: 49 | address: 127.0.0.1:8081 -------------------------------------------------------------------------------- /oam/simple2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: my-example-app 5 | annotations: 6 | description: "This is my app revision 2" 7 | spec: 8 | components: 9 | - name: userinfo 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/fake:1 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 4 17 | spread: 18 | - name: eastcoast 19 | requirements: 20 | zone: us-east-1 21 | weight: 80 22 | - name: westcoast 23 | requirements: 24 | zone: us-west-1 25 | weight: 20 26 | 27 | - name: webcap 28 | type: capability 29 | properties: 30 | image: wasmcloud.azurecr.io/httpserver:0.13.1 31 | traits: 32 | - type: link 33 | properties: 34 | target: 35 | name: userinfo 36 | config: [] 37 | namespace: wasi 38 | package: http 39 | interfaces: 40 | - incoming-handler 41 | source: 42 | config: [] 43 | 44 | - name: ledblinky 45 | type: capability 46 | properties: 47 | image: wasmcloud.azurecr.io/ledblinky:0.0.1 48 | traits: 49 | - type: spreadscaler 50 | properties: 51 | instances: 1 52 | spread: 53 | - name: haslights 54 | requirements: 55 | ledenabled: "true" 56 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/simple2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: my-example-app 5 | annotations: 6 | description: 'This is my app revision 2' 7 | spec: 8 | components: 9 | - name: userinfo 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/fake:1 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 4 17 | spread: 18 | - name: eastcoast 19 | requirements: 20 | zone: us-east-1 21 | weight: 80 22 | - name: westcoast 23 | requirements: 24 | zone: us-west-1 25 | weight: 20 26 | 27 | - name: webcap 28 | type: capability 29 | properties: 30 | image: wasmcloud.azurecr.io/httpserver:0.13.1 31 | traits: 32 | - type: link 33 | properties: 34 | target: 35 | name: userinfo 36 | config: [] 37 | namespace: wasi 38 | package: http 39 | interfaces: 40 | - incoming-handler 41 | source: 42 | config: [] 43 | 44 | - name: ledblinky 45 | type: capability 46 | properties: 47 | image: wasmcloud.azurecr.io/ledblinky:0.0.1 48 | traits: 49 | - type: spreadscaler 50 | properties: 51 | instances: 1 52 | spread: 53 | - name: haslights 54 | requirements: 55 | ledenabled: 'true' 56 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/shared_http_dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: shared-http-dev 5 | annotations: 6 | description: 'A Hello World app for testing, most basic HTTP link' 7 | spec: 8 | components: 9 | - name: hello 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 12 17 | - type: link 18 | properties: 19 | namespace: wasi 20 | package: http 21 | interfaces: [outgoing-handler] 22 | target: 23 | # Note that the name in this manifest does not have to be the same 24 | # as the name of the component in the shared manifest 25 | name: http-client-this 26 | - name: http-client-this 27 | type: capability 28 | properties: 29 | application: 30 | name: shared-http 31 | component: httpclient 32 | - name: httpserver 33 | type: capability 34 | properties: 35 | application: 36 | name: shared-http 37 | component: httpserver 38 | traits: 39 | - type: link 40 | properties: 41 | namespace: wasi 42 | package: http 43 | interfaces: [incoming-handler] 44 | target: 45 | name: hello 46 | source: 47 | config: 48 | - name: httpaddr 49 | properties: 50 | address: 0.0.0.0:8080 51 | -------------------------------------------------------------------------------- /.github/workflows/wit-wadm.yaml: -------------------------------------------------------------------------------- 1 | name: wit-wasmcloud-wadm-publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "wit-wasmcloud-wadm-v*" 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | packages: write 17 | steps: 18 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 19 | with: 20 | sparse-checkout: | 21 | wit 22 | .github 23 | - name: Extract tag context 24 | id: ctx 25 | run: | 26 | version=${GITHUB_REF_NAME#wit-wasmcloud-wadm-v} 27 | echo "version=${version}" >> "$GITHUB_OUTPUT" 28 | echo "tarball=wit-wasmcloud-wadm-${version}.tar.gz" >> "$GITHUB_OUTPUT" 29 | echo "version is ${version}" 30 | - uses: ./.github/actions/configure-wkg 31 | with: 32 | oci-username: ${{ github.repository_owner }} 33 | oci-password: ${{ secrets.GITHUB_TOKEN }} 34 | - name: Build 35 | run: wkg wit build --wit-dir wit/wadm -o package.wasm 36 | - name: Push version-tagged WebAssembly binary to GHCR 37 | run: wkg publish package.wasm 38 | - name: Package tarball for release 39 | run: | 40 | mkdir -p release/wit 41 | cp wit/wadm/*.wit release/wit/ 42 | tar cvzf ${{ steps.ctx.outputs.tarball }} -C release wit 43 | - name: Release 44 | uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4 45 | with: 46 | files: ${{ steps.ctx.outputs.tarball }} 47 | make_latest: "false" 48 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/shared/shared_component_dev.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: shared-component-dev 5 | annotations: 6 | description: 'A Hello World app for testing, most basic link' 7 | spec: 8 | components: 9 | # Link a component to a shared component 10 | - name: hello 11 | type: component 12 | properties: 13 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 14 | traits: 15 | - type: spreadscaler 16 | properties: 17 | instances: 12 18 | - type: link 19 | properties: 20 | namespace: custom 21 | package: package 22 | interfaces: [inter, face] 23 | target: 24 | name: component-dep 25 | # Shared component, link to a component in this application 26 | - name: component-dep 27 | type: component 28 | properties: 29 | application: 30 | name: shared-component 31 | component: link-to-meee 32 | traits: 33 | - type: link 34 | properties: 35 | namespace: custom 36 | package: package 37 | interfaces: [inter, face] 38 | target: 39 | name: hello 40 | config: 41 | - name: someconfig 42 | properties: 43 | foo: bar 44 | # Link a provider to a shared component 45 | - name: httpserver 46 | type: capability 47 | properties: 48 | image: ghcr.io/wasmcloud/http-server:0.23.0 49 | traits: 50 | - type: spreadscaler 51 | properties: 52 | instances: 1 53 | - type: link 54 | properties: 55 | namespace: wasi 56 | package: http 57 | interfaces: [incoming-handler] 58 | target: 59 | name: component-dep 60 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/all_hosts.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: hello-all-hosts 5 | annotations: 6 | description: 'This is my app' 7 | spec: 8 | policies: 9 | - name: test-policy 10 | type: test 11 | properties: 12 | test: 'data' 13 | components: 14 | - name: hello 15 | type: component 16 | properties: 17 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 18 | traits: 19 | - type: spreadscaler 20 | properties: 21 | instances: 5 22 | spread: 23 | - name: eastcoast 24 | requirements: 25 | region: us-brooks-east 26 | weight: 40 27 | - name: westcoast 28 | requirements: 29 | region: us-taylor-west 30 | weight: 40 31 | - name: the-moon 32 | requirements: 33 | region: moon 34 | weight: 20 35 | 36 | - name: httpserver 37 | type: capability 38 | properties: 39 | image: ghcr.io/wasmcloud/http-server:0.23.0 40 | traits: 41 | - type: spreadscaler 42 | properties: 43 | instances: 5 44 | spread: 45 | - name: eastcoast 46 | requirements: 47 | region: us-brooks-east 48 | weight: 40 49 | - name: westcoast 50 | requirements: 51 | region: us-taylor-west 52 | weight: 40 53 | - name: the-moon 54 | requirements: 55 | region: moon 56 | weight: 20 57 | - type: link 58 | properties: 59 | target: 60 | name: hello 61 | namespace: wasi 62 | package: http 63 | interfaces: ['incoming-handler'] 64 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e Tests Wadm 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | name: e2e 14 | runs-on: ubuntu-22.04 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | test: [e2e_multiple_hosts, e2e_upgrades, e2e_shared] 19 | 20 | steps: 21 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 22 | 23 | - name: Install latest Rust stable toolchain 24 | uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable 25 | with: 26 | toolchain: stable 27 | components: clippy, rustfmt 28 | 29 | # Cache: rust 30 | - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 31 | with: 32 | key: 'ubuntu-22.04-rust-cache' 33 | 34 | # If the test uses a docker compose file, pre-emptively pull images used in docker compose 35 | - name: Pull images for test ${{ matrix.test }} 36 | shell: bash 37 | run: | 38 | export DOCKER_COMPOSE_FILE=tests/docker-compose-${{ matrix.test }}.yaml; 39 | [[ -f "$DOCKER_COMPOSE_FILE" ]] && docker compose -f $DOCKER_COMPOSE_FILE pull; 40 | 41 | # Run e2e tests in a matrix for efficiency 42 | - name: Run tests ${{ matrix.test }} 43 | id: test 44 | env: 45 | WADM_E2E_TEST: ${{ matrix.test }} 46 | run: make test-individual-e2e 47 | 48 | # if the previous step fails, upload logs 49 | - name: Upload logs for debugging 50 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 51 | if: ${{ failure() && steps.test.outcome == 'failure' }} 52 | with: 53 | name: e2e-logs-${{ matrix.test }} 54 | path: ./tests/e2e_log/* 55 | # Be nice and only retain the logs for 7 days 56 | retention-days: 7 57 | -------------------------------------------------------------------------------- /crates/wadm/src/scaler/statusscaler.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_trait::async_trait; 3 | use wadm_types::{api::StatusInfo, TraitProperty}; 4 | 5 | use crate::{commands::Command, events::Event, scaler::Scaler}; 6 | 7 | /// The StatusScaler is a scaler that only reports a predefined status and does not perform any actions. 8 | /// It's primarily used as a placeholder for a scaler that wadm failed to initialize for reasons that 9 | /// couldn't be caught during deployment, and will not be fixed until a new version of the app is deployed. 10 | pub struct StatusScaler { 11 | id: String, 12 | kind: String, 13 | name: String, 14 | status: StatusInfo, 15 | } 16 | 17 | #[async_trait] 18 | impl Scaler for StatusScaler { 19 | fn id(&self) -> &str { 20 | &self.id 21 | } 22 | 23 | fn kind(&self) -> &str { 24 | &self.kind 25 | } 26 | 27 | fn name(&self) -> String { 28 | self.name.to_string() 29 | } 30 | 31 | async fn status(&self) -> StatusInfo { 32 | self.status.clone() 33 | } 34 | 35 | async fn update_config(&mut self, _config: TraitProperty) -> Result> { 36 | Ok(vec![]) 37 | } 38 | 39 | async fn handle_event(&self, _event: &Event) -> Result> { 40 | Ok(Vec::with_capacity(0)) 41 | } 42 | 43 | async fn reconcile(&self) -> Result> { 44 | Ok(Vec::with_capacity(0)) 45 | } 46 | 47 | async fn cleanup(&self) -> Result> { 48 | Ok(Vec::with_capacity(0)) 49 | } 50 | } 51 | 52 | impl StatusScaler { 53 | pub fn new( 54 | id: impl AsRef, 55 | kind: impl AsRef, 56 | name: impl AsRef, 57 | status: StatusInfo, 58 | ) -> Self { 59 | StatusScaler { 60 | id: id.as_ref().to_string(), 61 | kind: kind.as_ref().to_string(), 62 | name: name.as_ref().to_string(), 63 | status, 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /crates/wadm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wadm" 3 | description = "wasmCloud Application Deployment Manager: A tool for running Wasm applications in wasmCloud" 4 | version.workspace = true 5 | edition = "2021" 6 | authors = ["wasmCloud Team"] 7 | keywords = ["webassembly", "wasmcloud", "wadm"] 8 | license = "Apache-2.0" 9 | readme = "../../README.md" 10 | repository = "https://github.com/wasmcloud/wadm" 11 | 12 | [features] 13 | # Enables clap attributes on the wadm configuration struct 14 | cli = ["clap"] 15 | http_admin = ["http", "http-body-util", "hyper", "hyper-util"] 16 | default = [] 17 | 18 | [package.metadata.cargo-machete] 19 | ignored = ["cloudevents-sdk"] 20 | 21 | [dependencies] 22 | anyhow = { workspace = true } 23 | async-nats = { workspace = true } 24 | async-trait = { workspace = true } 25 | chrono = { workspace = true } 26 | clap = { workspace = true, optional = true, features = ["derive", "cargo", "env"]} 27 | cloudevents-sdk = { workspace = true } 28 | http = { workspace = true, features = ["std"], optional = true } 29 | http-body-util = { workspace = true, optional = true } 30 | hyper = { workspace = true, optional = true } 31 | hyper-util = { workspace = true, features = ["server"], optional = true } 32 | futures = { workspace = true } 33 | indexmap = { workspace = true, features = ["serde"] } 34 | nkeys = { workspace = true } 35 | semver = { workspace = true, features = ["serde"] } 36 | serde = { workspace = true } 37 | serde_json = { workspace = true } 38 | serde_yaml = { workspace = true } 39 | sha2 = { workspace = true } 40 | thiserror = { workspace = true } 41 | tokio = { workspace = true, features = ["full"] } 42 | tracing = { workspace = true, features = ["log"] } 43 | tracing-futures = { workspace = true } 44 | ulid = { workspace = true, features = ["serde"] } 45 | uuid = { workspace = true } 46 | wadm-types = { workspace = true } 47 | wasmcloud-control-interface = { workspace = true } 48 | wasmcloud-secrets-types = { workspace = true } 49 | 50 | [dev-dependencies] 51 | serial_test = "3" 52 | -------------------------------------------------------------------------------- /oam/kvcounter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: kvcounter-rust 5 | annotations: 6 | description: 'Kvcounter demo in Rust, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)' 7 | labels: 8 | app.oam.io/name: kvcounter-rust 9 | spec: 10 | components: 11 | - name: kvcounter 12 | type: component 13 | properties: 14 | image: file:///Users/brooks/github.com/wasmcloud/wadm/kvc/build/http_hello_world_s.wasm 15 | traits: 16 | # Govern the spread/scheduling of the component 17 | - type: spreadscaler 18 | properties: 19 | instances: 1 20 | # Compose with KVRedis for wasi:keyvalue calls 21 | - type: link 22 | properties: 23 | target: 24 | name: kvredis 25 | config: 26 | - name: redis-connect-local 27 | properties: 28 | url: redis://127.0.0.1:6379 29 | 30 | namespace: wasi 31 | package: keyvalue 32 | interfaces: 33 | - atomic 34 | - eventual 35 | 36 | # Add a capability provider that mediates HTTP access 37 | - name: httpserver 38 | type: capability 39 | properties: 40 | image: ghcr.io/wasmcloud/http-server:0.23.0 41 | traits: 42 | # Compose with component to handle wasi:http calls 43 | - type: link 44 | properties: 45 | target: 46 | name: kvcounter 47 | namespace: wasi 48 | package: http 49 | interfaces: 50 | - incoming-handler 51 | source: 52 | config: 53 | - name: listen-config 54 | properties: 55 | address: 127.0.0.1:8080 56 | # Add a capability provider that interfaces with the Redis key-value store 57 | - name: kvredis 58 | type: capability 59 | properties: 60 | image: ghcr.io/wasmcloud/keyvalue-redis:0.23.0 61 | -------------------------------------------------------------------------------- /tests/nats/nats-test.conf: -------------------------------------------------------------------------------- 1 | # This NATS server configuration uses insecure basic authentication for clarity, and sets up 2 | # a simple account structure for multitenant tests. It is not intended for production use. 3 | 4 | port: 5222 5 | 6 | jetstream { 7 | domain: wadm 8 | } 9 | 10 | leafnodes { 11 | port: 7422 12 | authorization { 13 | users = [ 14 | {user: a, password: a, account: A} 15 | {user: b, password: b, account: B} 16 | {user: wadm, password: wadm, account: WADM} 17 | ] 18 | } 19 | } 20 | 21 | accounts: { 22 | WADM: { 23 | users: [ 24 | {user: wadm, password: wadm} 25 | ] 26 | exports: [ 27 | {service: *.wadm.api.>, accounts: [A, B]} 28 | ] 29 | # Listen to wasmbus.evt events and wadm API commands from account A and B 30 | # Lattice IDs are unique, so we don't need to add account prefixes for them 31 | imports: [ 32 | {stream: {account: A, subject: wasmbus.evt.>}, prefix: Axxx} 33 | {stream: {account: B, subject: wasmbus.evt.>}, prefix: Ayyy} 34 | {service: {account: A, subject: wasmbus.ctl.>}, to: Axxx.wasmbus.ctl.>} 35 | {service: {account: B, subject: wasmbus.ctl.>}, to: Ayyy.wasmbus.ctl.>} 36 | ] 37 | } 38 | A: { 39 | users: [ 40 | {user: a, password: a} 41 | ] 42 | imports: [ 43 | {service: {account: WADM, subject: Axxx.wadm.api.>}, to: wadm.api.>} 44 | ] 45 | exports: [ 46 | {stream: wasmbus.evt.>, accounts: [WADM]} 47 | {service: wasmbus.ctl.>, accounts: [WADM], response_type: stream} 48 | ] 49 | } 50 | B: { 51 | users: [ 52 | {user: b, password: b} 53 | ] 54 | imports: [ 55 | {service: {account: WADM, subject: Ayyy.wadm.api.>}, to: wadm.api.>} 56 | ] 57 | exports: [ 58 | {stream: wasmbus.evt.>, accounts: [WADM]} 59 | {service: wasmbus.ctl.>, accounts: [WADM], response_type: stream} 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/deprecated-source-and-target-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: core.oam.dev/v1beta1 3 | kind: Application 4 | metadata: 5 | name: rust-http-blobstore 6 | annotations: 7 | version: v0.0.1 8 | description: 'HTTP Blobstore demo in Rust, using the WebAssembly Component Model and WebAssembly Interfaces Types (WIT)' 9 | wasmcloud.dev/authors: wasmCloud team 10 | wasmcloud.dev/source-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/rust/components/http-blobstore/wadm.yaml 11 | wasmcloud.dev/readme-md-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/rust/components/http-blobstore/README.md 12 | wasmcloud.dev/homepage: https://github.com/wasmCloud/wasmCloud/tree/main/examples/rust/components/http-blobstore 13 | wasmcloud.dev/categories: | 14 | http,http-server,rust,blobstore,object-storage,example 15 | spec: 16 | components: 17 | - name: http-blobstore 18 | type: component 19 | properties: 20 | image: ghcr.io/wasmcloud/components/http-blobstore-rust:0.2.0 21 | traits: 22 | - type: spreadscaler 23 | properties: 24 | instances: 1 25 | - type: link 26 | properties: 27 | target: blobstore-fs 28 | namespace: wasi 29 | package: blobstore 30 | interfaces: [blobstore] 31 | target_config: 32 | - name: root-directory 33 | properties: 34 | root: '/tmp' 35 | - name: httpserver 36 | type: capability 37 | properties: 38 | image: ghcr.io/wasmcloud/http-server:0.23.2 39 | traits: 40 | - type: link 41 | properties: 42 | target: http-blobstore 43 | namespace: wasi 44 | package: http 45 | interfaces: [incoming-handler] 46 | source_config: 47 | - name: default-http 48 | properties: 49 | address: 0.0.0.0:8000 50 | - name: blobstore-fs 51 | type: capability 52 | properties: 53 | image: ghcr.io/wasmcloud/blobstore-fs:0.10.1 -------------------------------------------------------------------------------- /tests/docker-compose-e2e_multitenant.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | # Set up a NATS core node and two leaf nodes which will connect with basic auth. 3 | nats-core: 4 | image: nats:2.9.16-alpine 5 | ports: 6 | - "127.0.0.1:5222:5222" 7 | - "127.0.0.1:7422:7422" 8 | command: ["-js", "-a", "0.0.0.0", "-c", "/nats/config/nats-test.conf"] 9 | volumes: 10 | - ./nats/:/nats/config 11 | nats-leaf-wadm: 12 | depends_on: 13 | - nats-core 14 | image: nats:2.9.16-alpine 15 | ports: 16 | - "127.0.0.1:4222:4222" 17 | command: ["-js", "-a", "0.0.0.0", "-c", "/nats/config/nats-leaf-wadm.conf"] 18 | volumes: 19 | - ./nats/:/nats/config 20 | nats-leaf-a: 21 | depends_on: 22 | - nats-core 23 | image: nats:2.9.16-alpine 24 | ports: 25 | - "127.0.0.1:4223:4222" 26 | command: ["-js", "-a", "0.0.0.0", "-c", "/nats/config/nats-leaf-a.conf"] 27 | volumes: 28 | - ./nats/:/nats/config 29 | nats-leaf-b: 30 | depends_on: 31 | - nats-core 32 | image: nats:2.9.16-alpine 33 | ports: 34 | - "127.0.0.1:4224:4222" 35 | command: ["-js", "-a", "0.0.0.0", "-c", "/nats/config/nats-leaf-b.conf"] 36 | volumes: 37 | - ./nats/:/nats/config 38 | 39 | # Have hosts in two different accounts 40 | wasmcloud_east: 41 | image: wasmcloud/wasmcloud:0.81.0 42 | depends_on: 43 | - nats-leaf-a 44 | deploy: 45 | replicas: 2 46 | environment: 47 | LC_ALL: en_US.UTF-8 48 | RUST_LOG: debug,hyper=info 49 | NATS_HOST: nats-leaf-a 50 | WASMCLOUD_LATTICE_PREFIX: wasmcloud-east 51 | WASMCLOUD_CLUSTER_SEED: SCAOGJWX53TGI4233T6GAXWYWBIB5ZDGPTCO6ODJQYELS52YCQCBQSRPA4 52 | HOST_region: us-brooks-east 53 | wasmcloud_west: 54 | image: wasmcloud/wasmcloud:0.81.0 55 | depends_on: 56 | - nats-leaf-b 57 | deploy: 58 | replicas: 2 59 | environment: 60 | LC_ALL: en_US.UTF-8 61 | RUST_LOG: debug,hyper=info 62 | WASMCLOUD_LATTICE_PREFIX: wasmcloud-west 63 | NATS_HOST: nats-leaf-b 64 | WASMCLOUD_CLUSTER_SEED: SCAOGJWX53TGI4233T6GAXWYWBIB5ZDGPTCO6ODJQYELS52YCQCBQSRPA4 65 | HOST_region: us-taylor-west 66 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/upgradedapp2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: updateapp 5 | annotations: 6 | description: 'Bees' 7 | spec: 8 | components: 9 | - name: dog-fetcher 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/dog-fetcher-rust:0.1.1 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 5 17 | - name: echo-messaging 18 | type: component 19 | properties: 20 | image: ghcr.io/wasmcloud/components/echo-messaging-rust:0.1.0 21 | traits: 22 | - type: spreadscaler 23 | properties: 24 | instances: 1 25 | - name: hello-world 26 | type: component 27 | properties: 28 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 29 | id: http_hello_world 30 | traits: 31 | - type: spreadscaler 32 | properties: 33 | instances: 3 34 | 35 | - name: httpserver 36 | type: capability 37 | properties: 38 | image: ghcr.io/wasmcloud/http-server:0.23.0 39 | id: http_server 40 | # Updated config 41 | config: 42 | - name: http-config 43 | properties: 44 | number: '1' 45 | traits: 46 | - type: link 47 | properties: 48 | name: hello 49 | target: 50 | name: hello-world 51 | namespace: wasi 52 | package: http 53 | interfaces: 54 | - incoming-handler 55 | # Updated link 56 | source: 57 | config: 58 | - name: hello-world-address 59 | properties: 60 | address: 0.0.0.0:8080 61 | - type: link 62 | properties: 63 | name: dog 64 | target: 65 | name: dog-fetcher 66 | namespace: wasi 67 | package: http 68 | interfaces: 69 | - incoming-handler 70 | source: 71 | config: 72 | - name: dog-fetcher-address 73 | properties: 74 | address: 0.0.0.0:8081 75 | -------------------------------------------------------------------------------- /crates/wadm/src/server/parser.rs: -------------------------------------------------------------------------------- 1 | use async_nats::HeaderMap; 2 | 3 | use wadm_types::Manifest; 4 | 5 | /// The name of the header in the NATS request to use for content type inference. The header value 6 | /// should be a valid MIME type 7 | pub const CONTENT_TYPE_HEADER: &str = "Content-Type"; 8 | 9 | // NOTE(thomastaylor312): If we do _anything_ else with mime types in the server, we should just 10 | // pull in the `mime` crate instead 11 | const YAML_MIME: &str = "application/yaml"; 12 | const JSON_MIME: &str = "application/json"; 13 | 14 | /// Parse the incoming bytes to a manifest 15 | /// 16 | /// This function takes the optional headers from a NATS request to use them as a type hint for 17 | /// parsing 18 | pub fn parse_manifest(data: Vec, headers: Option<&HeaderMap>) -> anyhow::Result { 19 | // There is far too much cloning here, but there is no way to just return a reference to a &str 20 | let content_type = headers 21 | .and_then(|map| map.get(CONTENT_TYPE_HEADER).cloned()) 22 | .map(|value| value.as_str().to_owned()); 23 | if let Some(content_type) = content_type { 24 | match content_type.as_str() { 25 | JSON_MIME => serde_json::from_slice(&data).map_err(anyhow::Error::from), 26 | YAML_MIME => serde_yaml::from_slice(&data).map_err(anyhow::Error::from), 27 | _ => { 28 | // If the user passed a non-supported mime type, we should let them know rather than 29 | // just falling back 30 | Err(anyhow::anyhow!( 31 | "Unsupported content type {content_type} given. Wadm supports YAML and JSON" 32 | )) 33 | } 34 | } 35 | } else { 36 | parse_yaml_or_json(data) 37 | } 38 | } 39 | 40 | /// Parse the bytes as yaml or json (in that order) 41 | fn parse_yaml_or_json(data: Vec) -> anyhow::Result { 42 | serde_yaml::from_slice(&data).or_else(|e| { 43 | serde_json::from_slice(&data).map_err(|err| { 44 | // Combine both errors in case one was a legit parsing failure due to invalid data 45 | anyhow::anyhow!("JSON parsing failed: {err:?}") 46 | .context(format!("YAML parsing failed: {e:?}")) 47 | }) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /wit/wadm/client.wit: -------------------------------------------------------------------------------- 1 | package wasmcloud:wadm@0.2.0; 2 | 3 | /// A Wadm client which interacts with the wadm api 4 | interface client { 5 | use types.{ 6 | version-info, 7 | status, 8 | model-summary, 9 | oam-manifest 10 | }; 11 | 12 | // Deploys a model to the WADM system. 13 | // If no lattice is provided, the default lattice name 'default' is used. 14 | deploy-model: func(model-name: string, version: option, lattice: option) -> result; 15 | 16 | // Undeploys a model from the WADM system. 17 | undeploy-model: func(model-name: string, lattice: option, non-destructive: bool) -> result<_, string>; 18 | 19 | // Stores the application manifest for later deploys. 20 | // Model is the full YAML or JSON string in this case 21 | // Returns the model name and version respectively. 22 | put-model: func(model: string, lattice: option) -> result, string>; 23 | 24 | /// Store an oam manifest directly for later deploys. 25 | put-manifest: func(manifest: oam-manifest, lattice: option) -> result, string>; 26 | 27 | // Retrieves the history of a given model name. 28 | get-model-history: func(model-name: string, lattice: option) -> result, string>; 29 | 30 | // Retrieves the status of a given model by name. 31 | get-model-status: func(model-name: string, lattice: option) -> result; 32 | 33 | // Retrieves details on a given model. 34 | get-model-details: func(model-name: string, version: option, lattice: option) -> result; 35 | 36 | // Deletes a model version from the WADM system. 37 | delete-model-version: func(model-name: string, version: option, lattice: option) -> result; 38 | 39 | // Retrieves all application manifests. 40 | get-models: func(lattice: option) -> result, string>; 41 | } 42 | 43 | interface handler { 44 | use types.{status-update}; 45 | 46 | // Callback handled to invoke a function when an update is received from a app status subscription 47 | handle-status-update: func(msg: status-update) -> result<_, string>; 48 | } 49 | -------------------------------------------------------------------------------- /crates/wadm/src/server/notifier.rs: -------------------------------------------------------------------------------- 1 | use cloudevents::Event as CloudEvent; 2 | use tracing::{instrument, trace}; 3 | use wadm_types::Manifest; 4 | 5 | use crate::{ 6 | events::{Event, ManifestPublished, ManifestUnpublished}, 7 | publisher::Publisher, 8 | }; 9 | 10 | /// A notifier that publishes changes about manifests with the given publisher 11 | pub struct ManifestNotifier

{ 12 | prefix: String, 13 | publisher: P, 14 | } 15 | 16 | impl ManifestNotifier

{ 17 | /// Creates a new notifier with the given prefix and publisher. This prefix should be something like 18 | /// `wadm.evt` that is used to form the full topic to send to 19 | pub fn new(prefix: &str, publisher: P) -> ManifestNotifier

{ 20 | let trimmer: &[_] = &['.', '>', '*']; 21 | ManifestNotifier { 22 | prefix: prefix.trim().trim_matches(trimmer).to_owned(), 23 | publisher, 24 | } 25 | } 26 | 27 | #[instrument(level = "trace", skip(self))] 28 | async fn send_event( 29 | &self, 30 | lattice_id: &str, 31 | event_subject_key: &str, 32 | event: Event, 33 | ) -> anyhow::Result<()> { 34 | let event: CloudEvent = event.try_into()?; 35 | // NOTE(thomastaylor312): A future improvement could be retries here 36 | trace!("Sending notification event"); 37 | self.publisher 38 | .publish( 39 | serde_json::to_vec(&event)?, 40 | Some(&format!("{}.{lattice_id}.{event_subject_key}", self.prefix)), 41 | ) 42 | .await 43 | } 44 | 45 | pub async fn deployed(&self, lattice_id: &str, manifest: Manifest) -> anyhow::Result<()> { 46 | self.send_event( 47 | lattice_id, 48 | "manifest_published", 49 | Event::ManifestPublished(ManifestPublished { manifest }), 50 | ) 51 | .await 52 | } 53 | 54 | pub async fn undeployed(&self, lattice_id: &str, name: &str) -> anyhow::Result<()> { 55 | self.send_event( 56 | lattice_id, 57 | "manifest_unpublished", 58 | Event::ManifestUnpublished(ManifestUnpublished { 59 | name: name.to_owned(), 60 | }), 61 | ) 62 | .await 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /oam/sqldbpostgres.yaml: -------------------------------------------------------------------------------- 1 | # Copied from https://github.com/wasmCloud/wasmCloud/blob/main/examples/rust/components/sqldb-postgres-query/wadm.yaml 2 | --- 3 | apiVersion: core.oam.dev/v1beta1 4 | kind: Application 5 | metadata: 6 | name: rust-sqldb-postgres-query 7 | annotations: 8 | version: v0.0.1 9 | description: | 10 | Demo WebAssembly component using the wasmCloud SQLDB Postgres provider via the wasmcloud:postgres WIT interface 11 | wasmcloud.dev/authors: wasmCloud team 12 | wasmcloud.dev/source-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/rust/components/sqldb-postgres-quer/wadm.yaml 13 | wasmcloud.dev/readme-md-url: https://github.com/wasmCloud/wasmCloud/blob/main/examples/rust/components/sqldb-postgres-quer/README.md 14 | wasmcloud.dev/homepage: https://github.com/wasmCloud/wasmCloud/tree/main/examples/rust/components/sqldb-postgres-quer 15 | wasmcloud.dev/categories: | 16 | database,sqldb,postgres,rust,example 17 | spec: 18 | components: 19 | - name: querier 20 | type: component 21 | properties: 22 | # To use the locally compiled code in this folder, use the line below instead after running `wash build`: 23 | # image: file://./build/sqldb_postgres_query_s.wasm 24 | image: ghcr.io/wasmcloud/components/sqldb-postgres-query-rust:0.1.0 25 | traits: 26 | # Govern the spread/scheduling of the component 27 | - type: spreadscaler 28 | properties: 29 | instances: 1 30 | # Establish a unidirectional link to the `sqldb-postgres` provider (the sqldb provider), 31 | # so the `querier` component can make use of sqldb functionality provided Postgres 32 | # (i.e. reading/writing to a database) 33 | - type: link 34 | properties: 35 | target: 36 | name: sqldb-postgres 37 | config: 38 | - name: default-postgres 39 | namespace: wasmcloud 40 | package: postgres 41 | interfaces: [query] 42 | 43 | # Add a capability provider that interacts with the filesystem 44 | - name: sqldb-postgres 45 | type: capability 46 | properties: 47 | image: ghcr.io/wasmcloud/sqldb-postgres:0.2.0 48 | config: 49 | - name: 'default-postgres' 50 | -------------------------------------------------------------------------------- /crates/wadm-types/wit/deps/wadm/client.wit: -------------------------------------------------------------------------------- 1 | package wasmcloud:wadm@0.2.0; 2 | 3 | /// A Wadm client which interacts with the wadm api 4 | interface client { 5 | use types.{ 6 | version-info, 7 | status, 8 | model-summary, 9 | oam-manifest 10 | }; 11 | 12 | // Deploys a model to the WADM system. 13 | // If no lattice is provided, the default lattice name 'default' is used. 14 | deploy-model: func(model-name: string, version: option, lattice: option) -> result; 15 | 16 | // Undeploys a model from the WADM system. 17 | undeploy-model: func(model-name: string, lattice: option, non-destructive: bool) -> result<_, string>; 18 | 19 | // Stores the application manifest for later deploys. 20 | // Model is the full YAML or JSON string in this case 21 | // Returns the model name and version respectively. 22 | put-model: func(model: string, lattice: option) -> result, string>; 23 | 24 | /// Store an oam manifest directly for later deploys. 25 | put-manifest: func(manifest: oam-manifest, lattice: option) -> result, string>; 26 | 27 | // Retrieves the history of a given model name. 28 | get-model-history: func(model-name: string, lattice: option) -> result, string>; 29 | 30 | // Retrieves the status of a given model by name. 31 | get-model-status: func(model-name: string, lattice: option) -> result; 32 | 33 | // Retrieves details on a given model. 34 | get-model-details: func(model-name: string, version: option, lattice: option) -> result; 35 | 36 | // Deletes a model version from the WADM system. 37 | delete-model-version: func(model-name: string, version: option, lattice: option) -> result; 38 | 39 | // Retrieves all application manifests. 40 | get-models: func(lattice: option) -> result, string>; 41 | } 42 | 43 | interface handler { 44 | use types.{status-update}; 45 | 46 | // Callback handled to invoke a function when an update is received from a app status subscription 47 | handle-status-update: func(msg: status-update) -> result<_, string>; 48 | } 49 | -------------------------------------------------------------------------------- /charts/wadm/values.yaml: -------------------------------------------------------------------------------- 1 | wadm: 2 | # replicas represents the number of copies of wadm to run 3 | replicas: 1 4 | # image represents the image and tag for running wadm 5 | image: 6 | repository: ghcr.io/wasmcloud/wadm 7 | pullPolicy: IfNotPresent 8 | # Overrides the image tag whose default is the chart appVersion. 9 | tag: "" 10 | config: 11 | apiPrefix: "" 12 | streamPrefix: "" 13 | cleanupInterval: "" 14 | hostId: "" 15 | logLevel: "" 16 | nats: 17 | server: "" 18 | jetstreamDomain: "" 19 | tlsCaFile: "" 20 | creds: 21 | jwt: "" 22 | seed: "" 23 | secretName: "" 24 | key: "nats.creds" 25 | maxJobs: "" 26 | stateBucket: "" 27 | manifestBucket: "" 28 | multitenant: false 29 | structuredLogging: false 30 | tracing: false 31 | tracingEndpoint: "" 32 | 33 | imagePullSecrets: [] 34 | nameOverride: "" 35 | fullnameOverride: "" 36 | 37 | additionalLabels: {} 38 | # app: wadm 39 | 40 | serviceAccount: 41 | # Specifies whether a service account should be created 42 | create: true 43 | # Automatically mount a ServiceAccount's API credentials? 44 | automount: true 45 | # Annotations to add to the service account 46 | annotations: {} 47 | # The name of the service account to use. 48 | # If not set and create is true, a name is generated using the fullname template 49 | name: "" 50 | 51 | podAnnotations: {} 52 | podLabels: {} 53 | 54 | podSecurityContext: {} 55 | # fsGroup: 1000 56 | 57 | securityContext: 58 | runAsUser: 1000 59 | runAsGroup: 1000 60 | runAsNonRoot: true 61 | allowPrivilegeEscalation: false 62 | capabilities: 63 | drop: 64 | - "ALL" 65 | seccompProfile: 66 | type: "RuntimeDefault" 67 | 68 | 69 | resources: {} 70 | # We usually recommend not to specify default resources and to leave this as a conscious 71 | # choice for the user. This also increases chances charts run on environments with little 72 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 73 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 74 | # limits: 75 | # cpu: 100m 76 | # memory: 128Mi 77 | # requests: 78 | # cpu: 100m 79 | # memory: 128Mi 80 | 81 | nodeSelector: {} 82 | 83 | tolerations: [] 84 | 85 | affinity: {} 86 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/upgradedapp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: updateapp 5 | annotations: 6 | description: 'Bees' 7 | spec: 8 | components: 9 | # Updated imageref component 10 | - name: dog-fetcher 11 | type: component 12 | properties: 13 | image: ghcr.io/wasmcloud/components/dog-fetcher-rust:0.1.1 14 | traits: 15 | - type: spreadscaler 16 | properties: 17 | instances: 5 18 | # Totally new component 19 | - name: echo-messaging 20 | type: component 21 | properties: 22 | image: ghcr.io/wasmcloud/components/echo-messaging-rust:0.1.0 23 | traits: 24 | - type: spreadscaler 25 | properties: 26 | instances: 1 27 | # Latest, no modifications needed, component 28 | - name: hello-world 29 | type: component 30 | properties: 31 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 32 | id: http_hello_world 33 | traits: 34 | - type: spreadscaler 35 | properties: 36 | instances: 3 37 | # kvredis component no longer present 38 | 39 | # Updated provider component 40 | - name: httpserver 41 | type: capability 42 | properties: 43 | image: ghcr.io/wasmcloud/http-server:0.23.0 44 | id: http_server 45 | traits: 46 | # Updated linkdef trait 47 | - type: link 48 | properties: 49 | name: hello 50 | target: 51 | name: hello-world 52 | namespace: wasi 53 | package: http 54 | interfaces: 55 | - incoming-handler 56 | source: 57 | config: 58 | - name: hello-world-address 59 | properties: 60 | address: 0.0.0.0:8082 61 | - type: link 62 | properties: 63 | name: dog 64 | target: 65 | name: dog-fetcher 66 | namespace: wasi 67 | package: http 68 | interfaces: 69 | - incoming-handler 70 | source: 71 | config: 72 | - name: dog-fetcher-address 73 | properties: 74 | address: 0.0.0.0:8081 75 | # Redis provider no longer present 76 | -------------------------------------------------------------------------------- /oam/simple1.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "core.oam.dev/v1beta1", 3 | "kind": "Application", 4 | "metadata": { 5 | "name": "my-example-app", 6 | "annotations": { 7 | "description": "This is my app" 8 | } 9 | }, 10 | "spec": { 11 | "components": [ 12 | { 13 | "name": "userinfo", 14 | "type": "actor", 15 | "properties": { 16 | "image": "wasmcloud.azurecr.io/fake:1" 17 | }, 18 | "traits": [ 19 | { 20 | "type": "spreadscaler", 21 | "properties": { 22 | "instances": 4, 23 | "spread": [ 24 | { 25 | "name": "eastcoast", 26 | "requirements": { 27 | "zone": "us-east-1" 28 | }, 29 | "weight": 80 30 | }, 31 | { 32 | "name": "westcoast", 33 | "requirements": { 34 | "zone": "us-west-1" 35 | }, 36 | "weight": 20 37 | } 38 | ] 39 | } 40 | } 41 | ] 42 | }, 43 | { 44 | "name": "webcap", 45 | "type": "capability", 46 | "properties": { 47 | "image": "wasmcloud.azurecr.io/httpserver:0.13.1" 48 | }, 49 | "traits": [ 50 | { 51 | "type": "link", 52 | "properties": { 53 | "target": "webcap", 54 | "namespace": "wasi", 55 | "package": "http", 56 | "interfaces": ["incoming-handler"], 57 | "name": "default" 58 | } 59 | } 60 | ] 61 | }, 62 | { 63 | "name": "ledblinky", 64 | "type": "capability", 65 | "properties": { 66 | "image": "wasmcloud.azurecr.io/ledblinky:0.0.1" 67 | }, 68 | "traits": [ 69 | { 70 | "type": "spreadscaler", 71 | "properties": { 72 | "instances": 1, 73 | "spread": [ 74 | { 75 | "name": "haslights", 76 | "requirements": { 77 | "ledenabled": "true" 78 | } 79 | } 80 | ] 81 | } 82 | } 83 | ] 84 | } 85 | ] 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /crates/wadm-client/src/loader.rs: -------------------------------------------------------------------------------- 1 | //! Various helpers and traits for loading and parsing manifests 2 | 3 | use std::{ 4 | future::Future, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use wadm_types::Manifest; 9 | 10 | use crate::{error::ClientError, Result}; 11 | 12 | /// A trait for loading a [`Manifest`] from a variety of sources. This is also used as a convenience 13 | /// trait in the client for easily passing in any type of Manifest 14 | pub trait ManifestLoader { 15 | fn load_manifest(self) -> impl Future>; 16 | } 17 | 18 | impl ManifestLoader for &Manifest { 19 | async fn load_manifest(self) -> Result { 20 | Ok(self.clone()) 21 | } 22 | } 23 | 24 | impl ManifestLoader for Manifest { 25 | async fn load_manifest(self) -> Result { 26 | Ok(self) 27 | } 28 | } 29 | 30 | impl ManifestLoader for Vec { 31 | async fn load_manifest(self) -> Result { 32 | parse_yaml_or_json(self).map_err(Into::into) 33 | } 34 | } 35 | 36 | impl ManifestLoader for &[u8] { 37 | async fn load_manifest(self) -> Result { 38 | parse_yaml_or_json(self).map_err(Into::into) 39 | } 40 | } 41 | 42 | // Helper macro for implementing `ManifestLoader` for anything that implements `AsRef` (which 43 | // results in a compiler error if we do it generically) 44 | macro_rules! impl_manifest_loader_for_path { 45 | ($($ty:ty),*) => { 46 | $( 47 | impl ManifestLoader for $ty { 48 | async fn load_manifest(self) -> Result { 49 | let raw = tokio::fs::read(self).await.map_err(|e| ClientError::ManifestLoad(e.into()))?; 50 | parse_yaml_or_json(raw).map_err(Into::into) 51 | } 52 | } 53 | )* 54 | }; 55 | } 56 | 57 | impl_manifest_loader_for_path!(&Path, &str, &String, String, PathBuf, &PathBuf); 58 | 59 | /// A simple function that attempts to parse the given bytes as YAML or JSON. This is used in the 60 | /// implementations of `ManifestLoader` 61 | pub fn parse_yaml_or_json( 62 | raw: impl AsRef<[u8]>, 63 | ) -> std::result::Result { 64 | // Attempt to parse as YAML first, then JSON 65 | serde_yaml::from_slice(raw.as_ref()) 66 | .or_else(|_| serde_json::from_slice(raw.as_ref())) 67 | .map_err(Into::into) 68 | } 69 | -------------------------------------------------------------------------------- /crates/wadm/src/publisher.rs: -------------------------------------------------------------------------------- 1 | //! A module that defines a generic publisher trait and several common publishers that can be passed 2 | //! into various structs in wadm. Often times this is used for testing, but it also allows for 3 | //! flexibility for others who may want to publish to other sources 4 | 5 | use async_nats::{jetstream::Context, Client}; 6 | 7 | #[async_trait::async_trait] 8 | pub trait Publisher { 9 | /// Publishes the given data to an optional destination (i.e. subject or topic). Implementors 10 | /// are responsible for documenting guarantees of delivery for published data 11 | /// 12 | /// The destination is optional for two reasons: Sometimes a client cannot be scoped to a 13 | /// specific topic and also, some implementations may not use subject/topic based delivery 14 | async fn publish(&self, data: Vec, destination: Option<&str>) -> anyhow::Result<()>; 15 | } 16 | 17 | /// The publisher implementation for a normal NATS client constrained to the given topic. This only 18 | /// has guarantees that the message was sent, not that it was received 19 | #[async_trait::async_trait] 20 | impl Publisher for Client { 21 | async fn publish(&self, data: Vec, destination: Option<&str>) -> anyhow::Result<()> { 22 | let subject = match destination { 23 | Some(s) => s.to_owned(), 24 | None => anyhow::bail!("NATS publishes require a destination"), 25 | }; 26 | self.publish(subject, data.into()) 27 | .await 28 | .map_err(anyhow::Error::from) 29 | } 30 | } 31 | 32 | /// The publisher implementation for a NATS jetstream client. This implementation will guarantee 33 | /// that a sent message is received by a stream 34 | #[async_trait::async_trait] 35 | impl Publisher for Context { 36 | async fn publish(&self, data: Vec, destination: Option<&str>) -> anyhow::Result<()> { 37 | let subject = match destination { 38 | Some(s) => s.to_owned(), 39 | None => anyhow::bail!("NATS publishes require a destination"), 40 | }; 41 | let ack = self 42 | .publish(subject, data.into()) 43 | .await 44 | .map_err(|e| anyhow::anyhow!("Unable to publish message").context(e))?; 45 | 46 | ack.await 47 | .map(|_| ()) 48 | .map_err(|e| anyhow::anyhow!("Unable to verify receipt of message").context(e)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /crates/wadm/src/events/data.rs: -------------------------------------------------------------------------------- 1 | use core::hash::{Hash, Hasher}; 2 | use std::collections::BTreeMap; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | /// All unique data needed to identify a provider. For this reason, this type implements PartialEq 7 | /// and Hash since it can serve as a key 8 | #[derive(Debug, Serialize, Deserialize, Default, Clone, Eq)] 9 | pub struct ProviderInfo { 10 | #[serde(alias = "public_key")] 11 | pub provider_id: String, 12 | #[serde(default)] 13 | pub provider_ref: String, 14 | #[serde(default)] 15 | pub annotations: BTreeMap, 16 | } 17 | 18 | impl PartialEq for ProviderInfo { 19 | fn eq(&self, other: &Self) -> bool { 20 | self.provider_id == other.provider_id 21 | } 22 | } 23 | 24 | // We don't hash on annotations here because this is only hashed for a hosts 25 | // inventory where these three pieces need to be unique regardless of annotations 26 | impl Hash for ProviderInfo { 27 | fn hash(&self, state: &mut H) { 28 | self.provider_id.hash(state); 29 | } 30 | } 31 | 32 | #[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)] 33 | pub struct ProviderClaims { 34 | pub expires_human: String, 35 | // TODO: Should we actually parse the nkey? 36 | pub issuer: String, 37 | pub name: String, 38 | pub not_before_human: String, 39 | #[serde( 40 | serialize_with = "super::ser::tags", 41 | deserialize_with = "super::deser::tags" 42 | )] 43 | pub tags: Option>, 44 | pub version: String, 45 | } 46 | 47 | #[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)] 48 | pub struct ProviderHealthCheckInfo { 49 | pub provider_id: String, 50 | pub host_id: String, 51 | } 52 | 53 | #[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq)] 54 | pub struct ComponentClaims { 55 | pub call_alias: Option, 56 | #[serde(default)] 57 | pub expires_human: String, 58 | // TODO: parse as nkey? 59 | #[serde(default)] 60 | pub issuer: String, 61 | #[serde(default)] 62 | pub name: String, 63 | #[serde(default)] 64 | pub not_before_human: String, 65 | pub revision: Option, 66 | // NOTE: This doesn't need a custom deserialize because unlike provider claims, these come out 67 | // in an array 68 | pub tags: Option>, 69 | pub version: Option, 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test Wadm 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-22.04] 18 | nats_version: [2.10.22] 19 | 20 | steps: 21 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 22 | 23 | - name: Install latest Rust stable toolchain 24 | uses: dtolnay/rust-toolchain@1ff72ee08e3cb84d84adba594e0a297990fc1ed3 # stable 25 | with: 26 | toolchain: stable 27 | components: clippy, rustfmt 28 | 29 | # Cache: rust 30 | - uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 31 | with: 32 | key: '${{ matrix.os }}-rust-cache' 33 | 34 | - name: Check that Wadm JSON Schema is up-to-date 35 | shell: bash 36 | run: | 37 | cargo run --bin wadm-schema 38 | if [ $(git diff --exit-code > /dev/null) ]; then 39 | echo 'Wadm JSON Schema is out of date. Please run `cargo run --bin wadm-schema` and commit the changes.' 40 | exit 1 41 | fi 42 | 43 | - name: install wash 44 | uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21 45 | with: 46 | tool: wash@0.38.0 47 | 48 | # GH Actions doesn't currently support passing args to service containers and there is no way 49 | # to use an environment variable to turn on jetstream for nats, so we manually start it here 50 | - name: Start NATS 51 | run: docker run --rm -d --name wadm-test -p 127.0.0.1:4222:4222 nats:${{ matrix.nats_version }} -js 52 | 53 | - name: Build 54 | run: | 55 | cargo build --all-features --all-targets --workspace 56 | 57 | # Make sure the wadm crate works well with feature combinations 58 | # The above command builds the workspace and tests with no features 59 | - name: Check wadm crate with features 60 | run: | 61 | cargo check -p wadm --no-default-features 62 | cargo check -p wadm --features cli 63 | cargo check -p wadm --features http_admin 64 | cargo check -p wadm --features cli,http_admin 65 | 66 | # Run all tests 67 | - name: Run tests 68 | run: | 69 | cargo test --workspace -- --nocapture 70 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/outdatedapp.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: updateapp 5 | annotations: 6 | description: 'According to all known laws of aviation' 7 | spec: 8 | components: 9 | - name: dog-fetcher 10 | type: component 11 | properties: 12 | image: ghcr.io/wasmcloud/components/dog-fetcher-rust:0.1.0 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 5 17 | 18 | - name: hello-world 19 | type: component 20 | properties: 21 | image: ghcr.io/wasmcloud/components/http-hello-world-rust:0.1.0 22 | id: http_hello_world 23 | traits: 24 | - type: spreadscaler 25 | properties: 26 | instances: 3 27 | 28 | - name: kvcounter 29 | type: component 30 | properties: 31 | image: ghcr.io/wasmcloud/components/http-keyvalue-counter-rust:0.1.0 32 | traits: 33 | - type: spreadscaler 34 | properties: 35 | instances: 3 36 | - type: link 37 | properties: 38 | target: 39 | name: redis 40 | config: 41 | - name: kvconfig 42 | properties: 43 | URL: redis://127.0.0.1:6379 44 | namespace: wasi 45 | package: keyvalue 46 | interfaces: 47 | - atomics 48 | - store 49 | 50 | - name: httpserver 51 | type: capability 52 | properties: 53 | image: ghcr.io/wasmcloud/http-server:0.20.1 54 | id: http_server 55 | traits: 56 | - type: link 57 | properties: 58 | name: hello 59 | target: 60 | name: hello-world 61 | namespace: wasi 62 | package: http 63 | interfaces: 64 | - incoming-handler 65 | source: 66 | config: 67 | - name: hello-world-address 68 | properties: 69 | address: 0.0.0.0:8080 70 | - type: link 71 | properties: 72 | name: dog 73 | target: 74 | name: dog-fetcher 75 | namespace: wasi 76 | package: http 77 | interfaces: 78 | - incoming-handler 79 | source: 80 | config: 81 | - name: dog-fetcher-address 82 | properties: 83 | address: 0.0.0.0:8081 84 | 85 | - name: redis 86 | type: capability 87 | properties: 88 | image: ghcr.io/wasmcloud/keyvalue-redis:0.26.0 89 | id: keyvalue_redis 90 | -------------------------------------------------------------------------------- /crates/wadm/src/connections.rs: -------------------------------------------------------------------------------- 1 | //! A module for connection pools and generators. This is needed because control interface clients 2 | //! (and possibly other things like nats connections in the future) are lattice scoped or need 3 | //! different credentials 4 | use wasmcloud_control_interface::{Client, ClientBuilder}; 5 | 6 | // Copied from https://github.com/wasmCloud/control-interface-client/blob/main/src/broker.rs#L1, not public 7 | const DEFAULT_TOPIC_PREFIX: &str = "wasmbus.ctl"; 8 | 9 | /// A client constructor for wasmCloud control interface clients, identified by a lattice ID 10 | // NOTE: Yes, this sounds java-y. Deal with it. 11 | #[derive(Clone)] 12 | pub struct ControlClientConstructor { 13 | client: async_nats::Client, 14 | /// The topic prefix to use for operations 15 | topic_prefix: Option, 16 | } 17 | 18 | impl ControlClientConstructor { 19 | /// Creates a new client pool that is all backed using the same NATS client and an optional 20 | /// topic prefix. The given NATS client should be using credentials that can access all desired 21 | /// lattices. 22 | pub fn new( 23 | client: async_nats::Client, 24 | topic_prefix: Option, 25 | ) -> ControlClientConstructor { 26 | ControlClientConstructor { 27 | client, 28 | topic_prefix, 29 | } 30 | } 31 | 32 | /// Get the client for the given lattice ID 33 | pub fn get_connection(&self, id: &str, multitenant_prefix: Option<&str>) -> Client { 34 | let builder = ClientBuilder::new(self.client.clone()).lattice(id); 35 | 36 | let builder = builder.topic_prefix(topic_prefix( 37 | multitenant_prefix, 38 | self.topic_prefix.as_deref(), 39 | )); 40 | 41 | builder.build() 42 | } 43 | } 44 | 45 | /// Returns the topic prefix to use for the given multitenant prefix and topic prefix. The 46 | /// default prefix is `wasmbus.ctl`. 47 | /// 48 | /// If running in multitenant mode, we listen to events on *.wasmbus.evt.*.> and need to send commands 49 | /// back to the '*' account. This match takes into account custom prefixes as well to support 50 | /// advanced use cases. 51 | /// 52 | /// This function does _not_ take into account whether or not wadm is running in multitenant mode, it's assumed 53 | /// that passing a Some() value for multitenant_prefix means that wadm is running in multitenant mode. 54 | fn topic_prefix(multitenant_prefix: Option<&str>, topic_prefix: Option<&str>) -> String { 55 | match (multitenant_prefix, topic_prefix) { 56 | (Some(mt), Some(prefix)) => format!("{}.{}", mt, prefix), 57 | (Some(mt), None) => format!("{}.{DEFAULT_TOPIC_PREFIX}", mt), 58 | (None, Some(prefix)) => prefix.to_string(), 59 | _ => DEFAULT_TOPIC_PREFIX.to_string(), 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /oam/README.md: -------------------------------------------------------------------------------- 1 | # wadm Open Application Model 2 | 3 | The wasmCloud Application Deployment Manager uses the [Open Application Model](https://oam.dev) to define application specifications. Because this specification is extensible and _platform agnostic_, it makes for an ideal way to represent applications with metadata specific to wasmCloud. 4 | 5 | ## wasmCloud OAM Components 6 | 7 | The following is a list of the `component`s wasmCloud has added to the model. 8 | 9 | - `component` - A WebAssembly component 10 | - `provider` - A capability provider 11 | 12 | ## wasmCloud OAM Traits 13 | 14 | The following is a list of the `traits` wasmCloud has added via customization to its application model. 15 | 16 | - `spreadscaler` - Defines the spread of instances of a particular entity across multiple hosts with affinity requirements 17 | - `link` - A link definition that describes a link between a component and a capability provider or a component and another component 18 | 19 | ## JSON Schema 20 | 21 | A JSON schema is automatically generated from our Rust structures and is at the root of the repository: [oam.schema.json](../oam.schema.json). You can regenerate the `oam.schema.json` file by running `cargo run --bin wadm-schema`. 22 | 23 | ## Example Application YAML 24 | 25 | The following is an example YAML file describing an application 26 | 27 | ```yaml 28 | apiVersion: core.oam.dev/v1beta1 29 | kind: Application 30 | metadata: 31 | name: my-example-app 32 | annotations: 33 | description: 'This is my app revision 2' 34 | spec: 35 | components: 36 | - name: userinfo 37 | type: component 38 | properties: 39 | image: wasmcloud.azurecr.io/fake:1 40 | traits: 41 | - type: spreadscaler 42 | properties: 43 | instances: 4 44 | spread: 45 | - name: eastcoast 46 | requirements: 47 | zone: us-east-1 48 | weight: 80 49 | - name: westcoast 50 | requirements: 51 | zone: us-west-1 52 | weight: 20 53 | 54 | - name: webcap 55 | type: capability 56 | properties: 57 | image: wasmcloud.azurecr.io/httpserver:0.13.1 58 | traits: 59 | - type: link 60 | properties: 61 | target: 62 | name: userinfo 63 | config: [] 64 | namespace: wasi 65 | package: http 66 | interfaces: 67 | - incoming-handler 68 | source: 69 | config: [] 70 | 71 | - name: ledblinky 72 | type: capability 73 | properties: 74 | image: wasmcloud.azurecr.io/ledblinky:0.0.1 75 | traits: 76 | - type: spreadscaler 77 | properties: 78 | instances: 1 79 | spread: 80 | - name: haslights 81 | requirements: 82 | ledenabled: 'true' 83 | # default weight is 100 84 | ``` 85 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/complex.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: complex 5 | annotations: 6 | description: 'This is my CRUDdy complex blobby app with all configuration possibilities' 7 | spec: 8 | policies: 9 | - name: whatever 10 | type: a-sample-policy 11 | properties: 12 | some: value 13 | another: kind 14 | components: 15 | - name: blobby 16 | type: component 17 | properties: 18 | image: ghcr.io/wasmcloud/components/blobby-rust:0.4.0 19 | id: littleblobbytables 20 | config: 21 | - name: defaultcode 22 | properties: 23 | http: '404' 24 | - name: blobby-default-configuration-values 25 | traits: 26 | - type: spreadscaler 27 | properties: 28 | instances: 5 29 | spread: 30 | - name: eastcoast 31 | requirements: 32 | region: us-brooks-east 33 | weight: 40 34 | - name: westcoast 35 | requirements: 36 | region: us-taylor-west 37 | weight: 40 38 | - type: link 39 | properties: 40 | namespace: wasi 41 | package: blobstore 42 | interfaces: [blobstore] 43 | target: 44 | name: fileserver 45 | config: 46 | - name: rootfs 47 | properties: 48 | root: /tmp 49 | 50 | - name: httpserver 51 | type: capability 52 | properties: 53 | image: ghcr.io/wasmcloud/http-server:0.23.0 54 | id: http_server 55 | traits: 56 | - type: spreadscaler 57 | properties: 58 | instances: 3 59 | spread: 60 | - name: westcoast 61 | requirements: 62 | region: us-taylor-west 63 | weight: 40 64 | - name: the-moon 65 | requirements: 66 | region: moon 67 | weight: 20 68 | - type: link 69 | properties: 70 | namespace: wasi 71 | package: http 72 | interfaces: [incoming-handler] 73 | target: 74 | name: blobby 75 | source: 76 | config: 77 | - name: httpaddr 78 | properties: 79 | address: 0.0.0.0:8081 80 | 81 | - name: fileserver 82 | type: capability 83 | properties: 84 | image: ghcr.io/wasmcloud/blobstore-fs:0.6.0 85 | id: fileserver 86 | config: 87 | - name: defaultfs 88 | properties: 89 | root: /tmp/blobby 90 | traits: 91 | - type: spreadscaler 92 | properties: 93 | instances: 1 94 | spread: 95 | - name: the-moon 96 | requirements: 97 | region: moon 98 | weight: 100 99 | -------------------------------------------------------------------------------- /crates/wadm-client/src/nats.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for creating a NATS client without exposing the NATS client in the API 2 | use std::path::PathBuf; 3 | 4 | use anyhow::{Context, Result}; 5 | use async_nats::{Client, ConnectOptions}; 6 | 7 | const DEFAULT_NATS_ADDR: &str = "nats://127.0.0.1:4222"; 8 | 9 | /// Creates a NATS client from the given options 10 | pub async fn get_client( 11 | url: Option, 12 | seed: Option, 13 | jwt: Option, 14 | creds_path: Option, 15 | ca_path: Option, 16 | ) -> Result { 17 | let mut opts = ConnectOptions::new(); 18 | opts = match (seed, jwt, creds_path) { 19 | (Some(seed), Some(jwt), None) => { 20 | let jwt = resolve_jwt(jwt).await?; 21 | let kp = std::sync::Arc::new(get_seed(seed).await?); 22 | 23 | opts.jwt(jwt, move |nonce| { 24 | let key_pair = kp.clone(); 25 | async move { key_pair.sign(&nonce).map_err(async_nats::AuthError::new) } 26 | }) 27 | } 28 | (None, None, Some(creds)) => opts.credentials_file(creds).await?, 29 | (None, None, None) => opts, 30 | _ => { 31 | // We shouldn't ever get here due to the requirements on the flags, but return a helpful error just in case 32 | return Err(anyhow::anyhow!( 33 | "Got incorrect combination of connection options. Should either have nothing set, a seed, a jwt, or a credentials file" 34 | )); 35 | } 36 | }; 37 | if let Some(ca) = ca_path { 38 | opts = opts.add_root_certificates(ca).require_tls(true); 39 | } 40 | opts.connect(url.unwrap_or_else(|| DEFAULT_NATS_ADDR.to_string())) 41 | .await 42 | .map_err(Into::into) 43 | } 44 | 45 | /// Takes a string that could be a raw seed, or a path and does all the necessary loading and parsing steps 46 | async fn get_seed(seed: String) -> Result { 47 | // MAGIC NUMBER: Length of a seed key 48 | let raw_seed = if seed.len() == 58 && seed.starts_with('S') { 49 | seed 50 | } else { 51 | tokio::fs::read_to_string(seed) 52 | .await 53 | .context("Unable to read seed file")? 54 | }; 55 | 56 | nkeys::KeyPair::from_seed(&raw_seed).map_err(anyhow::Error::from) 57 | } 58 | 59 | /// Resolves a JWT value by either returning the string itself if it's a valid JWT 60 | /// or by loading the contents of a file specified by the JWT value. 61 | async fn resolve_jwt(jwt_or_file: String) -> Result { 62 | if tokio::fs::metadata(&jwt_or_file) 63 | .await 64 | .map(|metadata| metadata.is_file()) 65 | .unwrap_or(false) 66 | { 67 | tokio::fs::read_to_string(jwt_or_file) 68 | .await 69 | .map_err(|e| anyhow::anyhow!("Error loading JWT from file: {e}")) 70 | } else { 71 | // We could do more validation on the JWT here, but if the JWT is invalid then 72 | // connecting will fail anyways 73 | Ok(jwt_or_file) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/duplicate_link_config_names.wadm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: my-example-app 5 | annotations: 6 | description: "This is my app" 7 | spec: 8 | components: 9 | - name: userinfo1 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/fake:1 13 | traits: 14 | - type: link 15 | properties: 16 | namespace: wasi 17 | package: keyvalue 18 | interfaces: [atomics, store] 19 | target: 20 | name: kvredis 21 | config: 22 | - name: redis-url 23 | properties: 24 | url: "redis://127.0.0.1:6379" 25 | # this config name is duplicated, but has no properties, 26 | # so it references an existing config 27 | - name: my_example_app-shared_redis 28 | 29 | - name: userinfo2 30 | type: component 31 | properties: 32 | image: wasmcloud.azurecr.io/fake:1 33 | traits: 34 | - type: link 35 | properties: 36 | namespace: wasi 37 | package: keyvalue 38 | interfaces: [atomics, store] 39 | target: 40 | name: kvredis 41 | config: 42 | - name: redis-url 43 | properties: 44 | url: "redis://127.0.0.1:6379" 45 | # this config name is duplicated, but has no properties, 46 | # so it references an existing config 47 | - name: my_example_app-shared_redis 48 | 49 | - name: webcap1 50 | type: capability 51 | properties: 52 | id: httpserver1 53 | image: wasmcloud.azurecr.io/httpserver:0.13.1 54 | traits: 55 | - type: link 56 | properties: 57 | namespace: wasi 58 | package: http 59 | interfaces: ["incoming-handler"] 60 | target: 61 | name: userinfo1 62 | source: 63 | config: 64 | - name: default-port 65 | properties: 66 | port: 0.0.0.0:8080 67 | - name: alternate-port 68 | properties: 69 | address: 0.0.0.0:8081 70 | - name: alternate-port 71 | properties: 72 | address: 0.0.0.0:8081 73 | 74 | - name: webcap2 75 | type: capability 76 | properties: 77 | id: httpserver2 78 | image: wasmcloud.azurecr.io/httpserver:0.14.1 79 | traits: 80 | - type: link 81 | properties: 82 | target: 83 | name: userinfo2 84 | namespace: wasi 85 | package: http 86 | interfaces: ["incoming-handler"] 87 | source: 88 | config: 89 | - name: default-port 90 | properties: 91 | address: 0.0.0.0:8080 92 | 93 | - name: kvredis 94 | type: capability 95 | properties: 96 | image: ghcr.io/wasmcloud/keyvalue-redis:0.28.1 97 | -------------------------------------------------------------------------------- /crates/wadm-client/src/topics.rs: -------------------------------------------------------------------------------- 1 | use wadm_types::api::{DEFAULT_WADM_TOPIC_PREFIX, WADM_STATUS_API_PREFIX}; 2 | 3 | /// A generator that uses various config options to generate the proper topic names for the wadm API 4 | pub struct TopicGenerator { 5 | topic_prefix: String, 6 | model_prefix: String, 7 | } 8 | 9 | impl TopicGenerator { 10 | /// Creates a new topic generator with a lattice ID and an optional API prefix 11 | pub fn new(lattice: &str, prefix: Option<&str>) -> TopicGenerator { 12 | let topic_prefix = format!( 13 | "{}.{}", 14 | prefix.unwrap_or(DEFAULT_WADM_TOPIC_PREFIX), 15 | lattice 16 | ); 17 | let model_prefix = format!("{}.model", topic_prefix); 18 | TopicGenerator { 19 | topic_prefix, 20 | model_prefix, 21 | } 22 | } 23 | 24 | /// Returns the full prefix for the topic, including the API prefix and the lattice ID 25 | pub fn prefix(&self) -> &str { 26 | &self.topic_prefix 27 | } 28 | 29 | /// Returns the full prefix for model operations (currently the only operations supported in the 30 | /// API) 31 | pub fn model_prefix(&self) -> &str { 32 | &self.model_prefix 33 | } 34 | 35 | /// Returns the full topic for a model put operation 36 | pub fn model_put_topic(&self) -> String { 37 | format!("{}.put", self.model_prefix()) 38 | } 39 | 40 | /// Returns the full topic for a model get operation 41 | pub fn model_get_topic(&self, model_name: &str) -> String { 42 | format!("{}.get.{model_name}", self.model_prefix()) 43 | } 44 | 45 | /// Returns the full topic for a model delete operation 46 | pub fn model_delete_topic(&self, model_name: &str) -> String { 47 | format!("{}.del.{model_name}", self.model_prefix()) 48 | } 49 | 50 | /// Returns the full topic for a model list operation 51 | pub fn model_list_topic(&self) -> String { 52 | format!("{}.list", self.model_prefix()) 53 | } 54 | 55 | /// Returns the full topic for listing the versions of a model 56 | pub fn model_versions_topic(&self, model_name: &str) -> String { 57 | format!("{}.versions.{model_name}", self.model_prefix()) 58 | } 59 | 60 | /// Returns the full topic for a model deploy operation 61 | pub fn model_deploy_topic(&self, model_name: &str) -> String { 62 | format!("{}.deploy.{model_name}", self.model_prefix()) 63 | } 64 | 65 | /// Returns the full topic for a model undeploy operation 66 | pub fn model_undeploy_topic(&self, model_name: &str) -> String { 67 | format!("{}.undeploy.{model_name}", self.model_prefix()) 68 | } 69 | 70 | /// Returns the full topic for getting a model status 71 | pub fn model_status_topic(&self, model_name: &str) -> String { 72 | format!("{}.status.{model_name}", self.model_prefix()) 73 | } 74 | 75 | /// Returns the full topic for WADM status subscriptions 76 | pub fn wadm_status_topic(&self, app_name: &str) -> String { 77 | // Extract just the lattice name from topic_prefix 78 | let lattice = self.topic_prefix.split('.').last().unwrap_or("default"); 79 | format!("{}.{}.{}", WADM_STATUS_API_PREFIX, lattice, app_name) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: '28 13 * * 3' 14 | push: 15 | branches: [ "main" ] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: "Checkout code" 35 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 36 | with: 37 | persist-credentials: false 38 | 39 | - name: "Run analysis" 40 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: "Upload artifact" 62 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v3.pre.node20 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard (optional). 69 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 70 | - name: "Upload to code-scanning" 71 | uses: github/codeql-action/upload-sarif@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.10 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /charts/wadm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "wadm.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "wadm.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "wadm.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "wadm.labels" -}} 37 | helm.sh/chart: {{ include "wadm.chart" . }} 38 | {{ include "wadm.selectorLabels" . }} 39 | app.kubernetes.io/component: wadm 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | app.kubernetes.io/part-of: wadm 45 | {{- with .Values.additionalLabels }} 46 | {{ . | toYaml }} 47 | {{- end }} 48 | {{- end }} 49 | 50 | {{/* 51 | Selector labels 52 | */}} 53 | {{- define "wadm.selectorLabels" -}} 54 | app.kubernetes.io/name: {{ include "wadm.name" . }} 55 | app.kubernetes.io/instance: {{ .Release.Name }} 56 | {{- end }} 57 | 58 | {{- define "wadm.nats.server" -}} 59 | - name: WADM_NATS_SERVER 60 | {{- if .Values.wadm.config.nats.server }} 61 | value: {{ .Values.wadm.config.nats.server | quote }} 62 | {{- else }} 63 | value: nats-headless.{{ .Release.Namespace }}.svc.cluster.local 64 | {{- end }} 65 | {{- end }} 66 | 67 | {{- define "wadm.nats.auth" -}} 68 | {{- if .Values.wadm.config.nats.creds.secretName -}} 69 | - name: WADM_NATS_CREDS_FILE 70 | value: {{ include "wadm.nats.creds_file_path" . | quote }} 71 | {{- else if and .Values.wadm.config.nats.creds.jwt .Values.wadm.config.nats.creds.seed -}} 72 | - name: WADM_NATS_NKEY 73 | value: {{ .Values.wadm.config.nats.creds.seed | quote }} 74 | - name: WADM_NATS_JWT 75 | value: {{ .Values.wadm.config.nats.creds.jwt | quote }} 76 | {{- end }} 77 | {{- end }} 78 | 79 | {{- define "wadm.nats.creds_file_path" }} 80 | {{- if .Values.wadm.config.nats.creds.secretName -}} 81 | /etc/nats-creds/nats.creds 82 | {{- end }} 83 | {{- end }} 84 | 85 | {{- define "wadm.nats.creds_volume_mount" -}} 86 | {{- if .Values.wadm.config.nats.creds.secretName -}} 87 | volumeMounts: 88 | - name: nats-creds-secret-volume 89 | mountPath: "/etc/nats-creds" 90 | readOnly: true 91 | {{- end }} 92 | {{- end }} 93 | 94 | {{- define "wadm.nats.creds_volume" -}} 95 | {{- with .Values.wadm.config.nats.creds -}} 96 | {{- if .secretName -}} 97 | volumes: 98 | - name: nats-creds-secret-volume 99 | secret: 100 | secretName: {{ .secretName }} 101 | items: 102 | - key: {{ .key }} 103 | path: "nats.creds" 104 | {{- end }} 105 | {{- end }} 106 | {{- end }} 107 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/incorrect_component.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: petclinic 5 | annotations: 6 | description: 'wasmCloud Pet Clinic Sample' 7 | spec: 8 | components: 9 | - name: ui 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/ui:0.3.2 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | spread: 17 | - name: uiclinicapp 18 | requirements: 19 | app: petclinic 20 | 21 | - name: ui 22 | type: component 23 | properties: 24 | image: wasmcloud.azurecr.io/customers:0.3.1 25 | traits: 26 | - type: linkdef 27 | properties: 28 | target: 29 | name: postgres 30 | values: 31 | uri: postgres://user:pass@your.db.host.com/petclinic 32 | - type: spreadscaler 33 | properties: 34 | instances: 1 35 | spread: 36 | - name: customersclinicapp 37 | requirements: 38 | app: petclinic 39 | 40 | - name: vets 41 | type: component 42 | properties: 43 | image: wasmcloud.azurecr.io/vets:0.3.1 44 | traits: 45 | - type: linkdef 46 | properties: 47 | target: 48 | name: postgres 49 | values: 50 | uri: postgres://user:pass@your.db.host.com/petclinic 51 | foo: bar 52 | - type: spreadscaler 53 | properties: 54 | instances: 1 55 | spread: 56 | - name: vetsclinicapp 57 | requirements: 58 | app: petclinic 59 | 60 | - name: vets 61 | type: component 62 | properties: 63 | image: wasmcloud.azurecr.io/visits:0.3.1 64 | traits: 65 | - type: linkdef 66 | properties: 67 | target: 68 | name: postgres 69 | values: 70 | uri: postgres://user:pass@your.db.host.com/petclinic 71 | 72 | - type: spreadscaler 73 | properties: 74 | instances: 1 75 | spread: 76 | - name: visitsclinicapp 77 | requirements: 78 | app: petclinic 79 | 80 | - name: clinicapi 81 | type: component 82 | properties: 83 | image: wasmcloud.azurecr.io/clinicapi:0.3.1 84 | traits: 85 | - type: spreadscaler 86 | properties: 87 | instances: 1 88 | spread: 89 | - name: clinicapp 90 | requirements: 91 | app: petclinic 92 | - type: linkdef 93 | properties: 94 | target: 95 | name: httpserver 96 | values: 97 | address: '0.0.0.0:8080' 98 | 99 | - name: httpserver 100 | type: capability 101 | properties: 102 | image: wasmcloud.azurecr.io/httpserver:0.16.2 103 | traits: 104 | - type: spreadscaler 105 | properties: 106 | instances: 1 107 | spread: 108 | - name: httpserverspread 109 | requirements: 110 | app: petclinic 111 | 112 | - name: postgres 113 | type: capability 114 | properties: 115 | image: wasmcloud.azurecr.io/sqldb-postgres:0.3.1 116 | traits: 117 | - type: spreadscaler 118 | properties: 119 | instances: 1 120 | spread: 121 | - name: postgresspread 122 | requirements: 123 | app: petclinic 124 | -------------------------------------------------------------------------------- /.github/workflows/chart.yml: -------------------------------------------------------------------------------- 1 | name: chart 2 | 3 | env: 4 | HELM_VERSION: v3.14.0 5 | CHART_TESTING_NAMESPACE: chart-testing 6 | 7 | on: 8 | push: 9 | tags: 10 | - 'chart-v[0-9].[0-9]+.[0-9]+' 11 | pull_request: 12 | paths: 13 | - 'charts/**' 14 | - '.github/workflows/chart.yml' 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | validate: 21 | runs-on: ubuntu-22.04 22 | 23 | steps: 24 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Fetch main branch for chart-testing 29 | run: | 30 | git fetch origin main:main 31 | 32 | - name: Set up Helm 33 | uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 34 | with: 35 | version: ${{ env.HELM_VERSION }} 36 | 37 | # Used by helm chart-testing below 38 | - name: Set up Python 39 | uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 40 | with: 41 | python-version: '3.12.2' 42 | 43 | - name: Set up chart-testing 44 | uses: helm/chart-testing-action@0d28d3144d3a25ea2cc349d6e59901c4ff469b3b # v2.7.0 45 | with: 46 | version: v3.10.1 47 | yamllint_version: 1.35.1 48 | yamale_version: 5.0.0 49 | 50 | - name: Run chart-testing (lint) 51 | run: | 52 | ct lint --config charts/wadm/ct.yaml 53 | 54 | - name: Create kind cluster 55 | uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0 56 | with: 57 | version: "v0.22.0" 58 | 59 | - name: Install nats in the test cluster 60 | run: | 61 | helm repo add nats https://nats-io.github.io/k8s/helm/charts/ 62 | helm repo update 63 | helm install nats nats/nats -f charts/wadm/ci/nats.yaml --namespace ${{ env.CHART_TESTING_NAMESPACE }} --create-namespace 64 | 65 | - name: Run chart-testing install / same namespace 66 | run: | 67 | ct install --config charts/wadm/ct.yaml --namespace ${{ env.CHART_TESTING_NAMESPACE }} 68 | 69 | - name: Run chart-testing install / across namespaces 70 | run: | 71 | ct install --config charts/wadm/ct.yaml --helm-extra-set-args "--set=wadm.config.nats.server=nats://nats-headless.${{ env.CHART_TESTING_NAMESPACE }}.svc.cluster.local" 72 | 73 | publish: 74 | if: ${{ startsWith(github.ref, 'refs/tags/chart-v') }} 75 | runs-on: ubuntu-22.04 76 | needs: validate 77 | permissions: 78 | contents: read 79 | packages: write 80 | 81 | steps: 82 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 83 | 84 | - name: Set up Helm 85 | uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 86 | with: 87 | version: ${{ env.HELM_VERSION }} 88 | 89 | - name: Package 90 | run: | 91 | helm package charts/wadm -d .helm-charts 92 | 93 | - name: Login to GHCR 94 | uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 95 | with: 96 | registry: ghcr.io 97 | username: ${{ github.repository_owner }} 98 | password: ${{ secrets.GITHUB_TOKEN }} 99 | 100 | - name: Lowercase the organization name for ghcr.io 101 | run: | 102 | echo "GHCR_REPO_NAMESPACE=${GITHUB_REPOSITORY_OWNER,,}" >>${GITHUB_ENV} 103 | 104 | - name: Publish 105 | run: | 106 | for chart in .helm-charts/*; do 107 | if [ -z "${chart:-}" ]; then 108 | break 109 | fi 110 | helm push "${chart}" "oci://ghcr.io/${{ env.GHCR_REPO_NAMESPACE }}/charts" 111 | done 112 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wadm-cli" 3 | description = "wasmCloud Application Deployment Manager: A tool for running Wasm applications in wasmCloud" 4 | version.workspace = true 5 | edition = "2021" 6 | authors = ["wasmCloud Team"] 7 | keywords = ["webassembly", "wasmcloud", "wadm"] 8 | license = "Apache-2.0" 9 | readme = "README.md" 10 | repository = "https://github.com/wasmcloud/wadm" 11 | default-run = "wadm" 12 | 13 | [workspace.package] 14 | version = "0.21.0" 15 | 16 | [features] 17 | default = [] 18 | # internal feature for e2e tests 19 | _e2e_tests = [] 20 | 21 | [workspace] 22 | members = ["crates/*"] 23 | 24 | [dependencies] 25 | anyhow = { workspace = true } 26 | clap = { workspace = true, features = ["derive", "cargo", "env"] } 27 | # One version back to avoid clashes with 0.10 of otlp 28 | opentelemetry = { workspace = true, features = ["rt-tokio"] } 29 | # 0.10 to avoid protoc dep 30 | opentelemetry-otlp = { workspace = true, features = [ 31 | "http-proto", 32 | "reqwest-client", 33 | ] } 34 | schemars = { workspace = true } 35 | serde_json = { workspace = true } 36 | tokio = { workspace = true, features = ["full"] } 37 | tracing = { workspace = true, features = ["log"] } 38 | tracing-opentelemetry = { workspace = true } 39 | tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } 40 | wadm = { workspace = true, features = ["cli", "http_admin"] } 41 | wadm-types = { workspace = true } 42 | 43 | [workspace.dependencies] 44 | anyhow = "1" 45 | async-nats = "0.39" 46 | async-trait = "0.1" 47 | bytes = "1" 48 | chrono = "0.4" 49 | clap = { version = "4", features = ["derive", "cargo", "env"] } 50 | cloudevents-sdk = "0.8" 51 | futures = "0.3" 52 | http = { version = "1", default-features = false } 53 | http-body-util = { version = "0.1", default-features = false } 54 | hyper = { version = "1", default-features = false } 55 | hyper-util = { version = "0.1", default-features = false } 56 | indexmap = { version = "2", features = ["serde"] } 57 | jsonschema = "0.29" 58 | lazy_static = "1" 59 | nkeys = "0.4.5" 60 | # One version back to avoid clashes with 0.10 of otlp 61 | opentelemetry = { version = "0.17", features = ["rt-tokio"] } 62 | # 0.10 to avoid protoc dep 63 | opentelemetry-otlp = { version = "0.10", features = [ 64 | "http-proto", 65 | "reqwest-client", 66 | ] } 67 | rand = { version = "0.9", features = ["small_rng"] } 68 | # NOTE(thomastaylor312): Pinning this temporarily to 1.10 due to transitive dependency with oci 69 | # crates that are pinned to 1.10 70 | regex = "~1.10" 71 | schemars = "0.8" 72 | semver = { version = "1.0.25", features = ["serde"] } 73 | serde = "1" 74 | serde_json = "1" 75 | serde_yaml = "0.9" 76 | sha2 = "0.10.9" 77 | thiserror = "2" 78 | tokio = { version = "1", default-features = false } 79 | tracing = { version = "0.1", features = ["log"] } 80 | tracing-futures = "0.2" 81 | tracing-opentelemetry = { version = "0.17" } 82 | tracing-subscriber = { version = "0.3.7", features = ["env-filter", "json"] } 83 | ulid = { version = "1", features = ["serde"] } 84 | utoipa = "5" 85 | uuid = "1" 86 | wadm = { version = "0.21", path = "./crates/wadm" } 87 | wadm-client = { version = "0.10", path = "./crates/wadm-client" } 88 | wadm-types = { version = "0.8", path = "./crates/wadm-types" } 89 | wasmcloud-control-interface = "2.4.0" 90 | wasmcloud-secrets-types = "0.5.0" 91 | wit-bindgen-wrpc = { version = "0.9", default-features = false } 92 | wit-bindgen = { version = "0.36.0", default-features = false } 93 | 94 | [dev-dependencies] 95 | async-nats = { workspace = true } 96 | chrono = { workspace = true } 97 | futures = { workspace = true } 98 | serde = { workspace = true } 99 | serde_json = { workspace = true } 100 | serde_yaml = { workspace = true } 101 | serial_test = "3" 102 | wadm-client = { workspace = true } 103 | wadm-types = { workspace = true } 104 | wasmcloud-control-interface = { workspace = true } 105 | testcontainers = "0.25" 106 | 107 | [build-dependencies] 108 | schemars = { workspace = true } 109 | serde_json = { workspace = true } 110 | wadm-types = { workspace = true } 111 | 112 | [[bin]] 113 | name = "wadm" 114 | path = "src/main.rs" 115 | 116 | [[bin]] 117 | name = "wadm-schema" 118 | path = "src/schema.rs" 119 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := bash 2 | .ONESHELL: 3 | .SHELLFLAGS := -eu -o pipefail -c 4 | .DELETE_ON_ERROR: 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rules 7 | MAKEFLAGS += --no-print-directory 8 | MAKEFLAGS += -S 9 | 10 | OS_NAME := $(shell uname -s | tr '[:upper:]' '[:lower:]') 11 | ifeq ($(OS_NAME),darwin) 12 | NC_FLAGS := -czt 13 | else 14 | NC_FLAGS := -Czt 15 | endif 16 | 17 | .DEFAULT: help 18 | 19 | CARGO ?= cargo 20 | CARGO_CLIPPY ?= cargo-clippy 21 | DOCKER ?= docker 22 | NATS ?= nats 23 | 24 | # Defaulting to the local registry since multi-arch containers have to be pushed 25 | WADM_TAG ?= localhost:5000/wasmcloud/wadm:latest 26 | # These should be either built locally or downloaded from a release. 27 | BIN_AMD64 ?= wadm-amd64 28 | BIN_ARM64 ?= wadm-aarch64 29 | 30 | help: ## Display this help 31 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_\-.*]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 32 | 33 | all: build test 34 | 35 | ########### 36 | # Tooling # 37 | ########### 38 | 39 | # Ensure that clippy is installed 40 | check-cargo-clippy: 41 | ifeq ("",$(shell command -v $(CARGO_CLIPPY))) 42 | $(error "ERROR: clippy is not installed (see: https://doc.rust-lang.org/clippy/installation.html)") 43 | endif 44 | 45 | ######### 46 | # Build # 47 | ######### 48 | 49 | lint: check-cargo-clippy ## Run code lint 50 | $(CARGO) fmt --all --check 51 | $(CARGO) clippy --all-features --all-targets --workspace 52 | 53 | build: ## Build wadm 54 | $(CARGO) build --bin wadm 55 | 56 | build-docker: ## Build wadm docker image 57 | ifndef BIN_AMD64 58 | $(error BIN_AMD64 is not set, required for docker building) 59 | endif 60 | ifndef BIN_ARM64 61 | $(error BIN_ARM64 is not set, required for docker building) 62 | endif 63 | 64 | $(DOCKER) buildx build --platform linux/amd64,linux/arm64 \ 65 | --build-arg BIN_AMD64=$(BIN_AMD64) \ 66 | --build-arg BIN_ARM64=$(BIN_ARM64) \ 67 | -t $(WADM_TAG) \ 68 | --push . 69 | 70 | 71 | ######## 72 | # Test # 73 | ######## 74 | 75 | # An optional specific test for cargo to target 76 | 77 | CARGO_TEST_TARGET ?= 78 | 79 | test:: ## Run tests 80 | ifeq ($(shell nc $(NC_FLAGS) -w1 127.0.0.1 4222 || echo fail),fail) 81 | $(DOCKER) run --rm -d --name wadm-test -p 127.0.0.1:4222:4222 nats:2.10 -js 82 | $(CARGO) test $(CARGO_TEST_TARGET) -- --nocapture 83 | $(DOCKER) stop wadm-test 84 | else 85 | $(CARGO) test $(CARGO_TEST_TARGET) -- --nocapture 86 | endif 87 | 88 | test-e2e:: ## Run e2e tests 89 | ifeq ($(shell nc $(NC_FLAGS) -w1 127.0.0.1 4222 || echo fail),fail) 90 | @$(MAKE) build 91 | @# Reenable this once we've enabled all tests 92 | @# RUST_BACKTRACE=1 $(CARGO) test --test e2e_multitenant --features _e2e_tests -- --nocapture 93 | RUST_BACKTRACE=1 $(CARGO) test --test e2e_multiple_hosts --features _e2e_tests -- --nocapture 94 | RUST_BACKTRACE=1 $(CARGO) test --test e2e_upgrades --features _e2e_tests -- --nocapture 95 | else 96 | @echo "WARN: Not running e2e tests. NATS must not be currently running" 97 | exit 1 98 | endif 99 | 100 | test-individual-e2e:: ## Runs an individual e2e test based on the WADM_E2E_TEST env var 101 | ifeq ($(shell nc $(NC_FLAGS) -w1 127.0.0.1 4222 || echo fail),fail) 102 | @$(MAKE) build 103 | RUST_BACKTRACE=1 $(CARGO) test --test $(WADM_E2E_TEST) --features _e2e_tests -- --nocapture 104 | else 105 | @echo "WARN: Not running e2e tests. NATS must not be currently running" 106 | exit 1 107 | endif 108 | 109 | ########### 110 | # Cleanup # 111 | ########### 112 | 113 | stream-cleanup: ## Removes all streams that wadm creates 114 | -$(NATS) stream del wadm_commands --force 115 | -$(NATS) stream del wadm_events --force 116 | -$(NATS) stream del wadm_event_consumer --force 117 | -$(NATS) stream del wadm_notify --force 118 | -$(NATS) stream del wadm_status --force 119 | -$(NATS) stream del KV_wadm_state --force 120 | -$(NATS) stream del KV_wadm_manifests --force 121 | 122 | .PHONY: check-cargo-clippy lint build build-watch test stream-cleanup clean test-e2e test 123 | -------------------------------------------------------------------------------- /tests/fixtures/manifests/duplicate_component.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: core.oam.dev/v1beta1 2 | kind: Application 3 | metadata: 4 | name: petclinic 5 | annotations: 6 | description: "wasmCloud Pet Clinic Sample" 7 | spec: 8 | components: 9 | - name: ui 10 | type: component 11 | properties: 12 | image: wasmcloud.azurecr.io/ui:0.3.2 13 | traits: 14 | - type: spreadscaler 15 | properties: 16 | instances: 1 17 | spread: 18 | - name: uiclinicapp 19 | requirements: 20 | app: petclinic 21 | 22 | - name: ui 23 | type: component 24 | properties: 25 | image: wasmcloud.azurecr.io/customers:0.3.1 26 | traits: 27 | - type: linkdef 28 | properties: 29 | target: 30 | name: postgres 31 | config: 32 | - name: petclinic-connect 33 | properties: 34 | uri: postgres://user:pass@your.db.host.com/petclinic 35 | namespace: wasmcloud 36 | package: sql 37 | interfaces: ["query"] 38 | - type: spreadscaler 39 | properties: 40 | instances: 1 41 | spread: 42 | - name: customersclinicapp 43 | requirements: 44 | app: petclinic 45 | 46 | - name: vets 47 | type: component 48 | properties: 49 | image: wasmcloud.azurecr.io/vets:0.3.1 50 | traits: 51 | - type: linkdef 52 | properties: 53 | target: 54 | name: postgres 55 | config: 56 | - name: foobar 57 | - name: petclinic-connect 58 | properties: 59 | uri: postgres://user:pass@your.db.host.com/petclinic 60 | namespace: wasmcloud 61 | package: sql 62 | interfaces: ["query"] 63 | - type: spreadscaler 64 | properties: 65 | instances: 1 66 | spread: 67 | - name: vetsclinicapp 68 | requirements: 69 | app: petclinic 70 | 71 | - name: vets 72 | type: component 73 | properties: 74 | image: wasmcloud.azurecr.io/visits:0.3.1 75 | traits: 76 | - type: linkdef 77 | properties: 78 | target: 79 | name: postgres 80 | config: 81 | - name: petclinic-connect 82 | properties: 83 | uri: postgres://user:pass@your.db.host.com/petclinic 84 | namespace: wasmcloud 85 | package: sql 86 | interfaces: ["query"] 87 | 88 | - type: spreadscaler 89 | properties: 90 | instances: 1 91 | spread: 92 | - name: visitsclinicapp 93 | requirements: 94 | app: petclinic 95 | 96 | - name: clinicapi 97 | type: component 98 | properties: 99 | image: wasmcloud.azurecr.io/clinicapi:0.3.1 100 | traits: 101 | - type: spreadscaler 102 | properties: 103 | instances: 1 104 | spread: 105 | - name: clinicapp 106 | requirements: 107 | app: petclinic 108 | 109 | - name: httpserver 110 | type: capability 111 | properties: 112 | image: wasmcloud.azurecr.io/httpserver:0.16.2 113 | traits: 114 | - type: spreadscaler 115 | properties: 116 | instances: 1 117 | spread: 118 | - name: httpserverspread 119 | requirements: 120 | app: petclinic 121 | - type: linkdef 122 | properties: 123 | target: 124 | name: clinicapi 125 | namespace: wasi 126 | package: http 127 | interfaces: ["incoming-handler"] 128 | source: 129 | config: 130 | - name: default-port 131 | properties: 132 | address: "0.0.0.0:8080" 133 | 134 | - name: postgres 135 | type: capability 136 | properties: 137 | image: wasmcloud.azurecr.io/sqldb-postgres:0.3.1 138 | traits: 139 | - type: spreadscaler 140 | properties: 141 | instances: 1 142 | spread: 143 | - name: postgresspread 144 | requirements: 145 | app: petclinic 146 | -------------------------------------------------------------------------------- /charts/wadm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "wadm.fullname" . }} 5 | labels: 6 | {{- include "wadm.labels" . | nindent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicas }} 9 | selector: 10 | matchLabels: 11 | {{- include "wadm.selectorLabels" . | nindent 6 }} 12 | template: 13 | metadata: 14 | {{- with .Values.podAnnotations }} 15 | annotations: 16 | {{- toYaml . | nindent 8 }} 17 | {{- end }} 18 | labels: 19 | {{- include "wadm.labels" . | nindent 8 }} 20 | {{- with .Values.podLabels }} 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | spec: 24 | {{- with .Values.imagePullSecrets }} 25 | imagePullSecrets: 26 | {{- toYaml . | nindent 8 }} 27 | {{- end }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.wadm.image.repository }}:{{ .Values.wadm.image.tag | default .Chart.AppVersion }}" 35 | imagePullPolicy: {{ .Values.wadm.image.pullPolicy }} 36 | env: 37 | {{- include "wadm.nats.server" . | nindent 12 }} 38 | {{- include "wadm.nats.auth" . | nindent 12 }} 39 | {{- if .Values.wadm.config.nats.tlsCaFile }} 40 | - name: WADM_NATS_TLS_CA_FILE 41 | value: {{ .Values.wadm.config.nats.tlsCaFile | quote }} 42 | {{- end }} 43 | {{- if .Values.wadm.config.hostId }} 44 | - name: WADM_HOST_ID 45 | value: {{ .Values.wadm.config.hostId | quote }} 46 | {{- end }} 47 | {{- if .Values.wadm.config.structuredLogging }} 48 | - name: WADM_STRUCTURED_LOGGING 49 | value: {{ .Values.wadm.config.structuredLogging | quote }} 50 | {{- end }} 51 | {{- if .Values.wadm.config.tracing }} 52 | - name: WADM_TRACING_ENABLED 53 | value: {{ .Values.wadm.config.tracing | quote }} 54 | {{- end }} 55 | {{- if .Values.wadm.config.tracingEndpoint }} 56 | - name: WADM_TRACING_ENDPOINT 57 | value: {{ .Values.wadm.config.tracingEndpoint | quote }} 58 | {{- end }} 59 | {{- if .Values.wadm.config.nats.jetstreamDomain }} 60 | - name: WADM_JETSTREAM_DOMAIN 61 | value: {{ .Values.wadm.config.nats.jetstreamDomain | quote }} 62 | {{- end }} 63 | {{- if .Values.wadm.config.maxJobs }} 64 | - name: WADM_MAX_JOBS 65 | value: {{ .Values.wadm.config.maxJobs }} 66 | {{- end }} 67 | {{- if .Values.wadm.config.stateBucket }} 68 | - name: WADM_STATE_BUCKET_NAME 69 | value: {{ .Values.wadm.config.stateBucket | quote }} 70 | {{- end }} 71 | {{- if .Values.wadm.config.manifestBucket }} 72 | - name: WADM_MANIFEST_BUCKET_NAME 73 | value: {{ .Values.wadm.config.manifestBucket | quote }} 74 | {{- end }} 75 | {{- if .Values.wadm.config.cleanupInterval }} 76 | - name: WADM_CLEANUP_INTERVAL 77 | value: {{ .Values.wadm.config.cleanupInterval }} 78 | {{- end }} 79 | {{- if .Values.wadm.config.apiPrefix }} 80 | - name: WADM_API_PREFIX 81 | value: {{ .Values.wadm.config.apiPrefix }} 82 | {{- end }} 83 | {{- if .Values.wadm.config.streamPrefix }} 84 | - name: WADM_STREAM_PREFIX 85 | value: {{ .Values.wadm.config.streamPrefix }} 86 | {{- end }} 87 | {{- if .Values.wadm.config.multitenant }} 88 | - name: WADM_MULTITENANT 89 | value: {{ .Values.wadm.config.multitenant | quote }} 90 | {{- end }} 91 | resources: 92 | {{- toYaml .Values.resources | nindent 12 }} 93 | {{- include "wadm.nats.creds_volume_mount" . | nindent 10 -}} 94 | {{- include "wadm.nats.creds_volume" . | nindent 6 -}} 95 | {{- with .Values.nodeSelector }} 96 | nodeSelector: 97 | {{- toYaml . | nindent 8 }} 98 | {{- end }} 99 | {{- with .Values.affinity }} 100 | affinity: 101 | {{- toYaml . | nindent 8 }} 102 | {{- end }} 103 | {{- with .Values.tolerations }} 104 | tolerations: 105 | {{- toYaml . | nindent 8 }} 106 | {{- end }} 107 | -------------------------------------------------------------------------------- /src/logging.rs: -------------------------------------------------------------------------------- 1 | use std::io::IsTerminal; 2 | 3 | use opentelemetry::sdk::{ 4 | trace::{IdGenerator, Sampler}, 5 | Resource, 6 | }; 7 | use opentelemetry_otlp::{Protocol, WithExportConfig}; 8 | use tracing::{Event as TracingEvent, Subscriber}; 9 | use tracing_subscriber::fmt::{ 10 | format::{Format, Full, Json, JsonFields, Writer}, 11 | time::SystemTime, 12 | FmtContext, FormatEvent, FormatFields, 13 | }; 14 | use tracing_subscriber::{layer::SubscriberExt, registry::LookupSpan, EnvFilter, Layer}; 15 | 16 | const TRACING_PATH: &str = "/v1/traces"; 17 | 18 | /// A struct that allows us to dynamically choose JSON formatting without using dynamic dispatch. 19 | /// This is just so we avoid any sort of possible slow down in logging code 20 | enum JsonOrNot { 21 | Not(Format), 22 | Json(Format), 23 | } 24 | 25 | impl FormatEvent for JsonOrNot 26 | where 27 | S: Subscriber + for<'lookup> LookupSpan<'lookup>, 28 | N: for<'writer> FormatFields<'writer> + 'static, 29 | { 30 | fn format_event( 31 | &self, 32 | ctx: &FmtContext<'_, S, N>, 33 | writer: Writer<'_>, 34 | event: &TracingEvent<'_>, 35 | ) -> std::fmt::Result 36 | where 37 | S: Subscriber + for<'a> LookupSpan<'a>, 38 | { 39 | match self { 40 | JsonOrNot::Not(f) => f.format_event(ctx, writer, event), 41 | JsonOrNot::Json(f) => f.format_event(ctx, writer, event), 42 | } 43 | } 44 | } 45 | 46 | pub fn configure_tracing( 47 | structured_logging: bool, 48 | tracing_enabled: bool, 49 | tracing_endpoint: Option, 50 | ) { 51 | let env_filter_layer = get_env_filter(); 52 | let log_layer = get_log_layer(structured_logging); 53 | let subscriber = tracing_subscriber::Registry::default() 54 | .with(env_filter_layer) 55 | .with(log_layer); 56 | if !tracing_enabled && tracing_endpoint.is_none() { 57 | if let Err(e) = tracing::subscriber::set_global_default(subscriber) { 58 | eprintln!("Logger/tracer was already initted, continuing: {}", e); 59 | } 60 | return; 61 | } 62 | 63 | let mut tracing_endpoint = 64 | tracing_endpoint.unwrap_or_else(|| format!("http://localhost:4318{}", TRACING_PATH)); 65 | if !tracing_endpoint.ends_with(TRACING_PATH) { 66 | tracing_endpoint.push_str(TRACING_PATH); 67 | } 68 | let res = match opentelemetry_otlp::new_pipeline() 69 | .tracing() 70 | .with_exporter( 71 | opentelemetry_otlp::new_exporter() 72 | .http() 73 | .with_endpoint(tracing_endpoint) 74 | .with_protocol(Protocol::HttpBinary), 75 | ) 76 | .with_trace_config( 77 | opentelemetry::sdk::trace::config() 78 | .with_sampler(Sampler::AlwaysOn) 79 | .with_id_generator(IdGenerator::default()) 80 | .with_max_events_per_span(64) 81 | .with_max_attributes_per_span(16) 82 | .with_max_events_per_span(16) 83 | .with_resource(Resource::new(vec![opentelemetry::KeyValue::new( 84 | "service.name", 85 | "wadm", 86 | )])), 87 | ) 88 | .install_batch(opentelemetry::runtime::Tokio) 89 | { 90 | Ok(t) => tracing::subscriber::set_global_default( 91 | subscriber.with(tracing_opentelemetry::layer().with_tracer(t)), 92 | ), 93 | Err(e) => { 94 | eprintln!( 95 | "Unable to configure OTEL tracing, defaulting to logging only: {:?}", 96 | e 97 | ); 98 | tracing::subscriber::set_global_default(subscriber) 99 | } 100 | }; 101 | if let Err(e) = res { 102 | eprintln!("Logger/tracer was already initted, continuing: {}", e); 103 | } 104 | } 105 | 106 | fn get_log_layer(structured_logging: bool) -> Box + Send + Sync + 'static> 107 | where 108 | S: for<'a> tracing_subscriber::registry::LookupSpan<'a>, 109 | S: tracing::Subscriber, 110 | { 111 | let log_layer = tracing_subscriber::fmt::layer() 112 | .with_writer(std::io::stderr) 113 | .with_ansi(std::io::stderr().is_terminal()); 114 | if structured_logging { 115 | Box::new( 116 | log_layer 117 | .event_format(JsonOrNot::Json(Format::default().json())) 118 | .fmt_fields(JsonFields::default()), 119 | ) 120 | } else { 121 | Box::new(log_layer.event_format(JsonOrNot::Not(Format::default()))) 122 | } 123 | } 124 | 125 | fn get_env_filter() -> EnvFilter { 126 | EnvFilter::try_from_default_env().unwrap_or_else(|e| { 127 | eprintln!("RUST_LOG was not set or the given directive was invalid: {e:?}\nDefaulting logger to `info` level"); 128 | EnvFilter::default().add_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) 129 | }) 130 | } 131 | -------------------------------------------------------------------------------- /crates/wadm/src/workers/command.rs: -------------------------------------------------------------------------------- 1 | use tracing::{instrument, trace}; 2 | 3 | use crate::{ 4 | commands::*, 5 | consumers::{ 6 | manager::{WorkError, WorkResult, Worker}, 7 | ScopedMessage, 8 | }, 9 | }; 10 | 11 | use super::insert_managed_annotations; 12 | 13 | /// A worker implementation for handling incoming commands 14 | #[derive(Clone)] 15 | pub struct CommandWorker { 16 | client: wasmcloud_control_interface::Client, 17 | } 18 | 19 | impl CommandWorker { 20 | /// Creates a new command worker with the given connection pool. 21 | pub fn new(ctl_client: wasmcloud_control_interface::Client) -> CommandWorker { 22 | CommandWorker { client: ctl_client } 23 | } 24 | } 25 | 26 | #[async_trait::async_trait] 27 | impl Worker for CommandWorker { 28 | type Message = Command; 29 | 30 | #[instrument(level = "trace", skip_all)] 31 | async fn do_work(&self, mut message: ScopedMessage) -> WorkResult<()> { 32 | let res = match message.as_ref() { 33 | Command::ScaleComponent(component) => { 34 | trace!(command = ?component, "Handling scale component command"); 35 | // Order here is intentional to prevent scalers from overwriting managed annotations 36 | let mut annotations = component.annotations.clone(); 37 | insert_managed_annotations(&mut annotations, &component.model_name); 38 | self.client 39 | .scale_component( 40 | &component.host_id, 41 | &component.reference, 42 | &component.component_id, 43 | component.count, 44 | Some(annotations.into_iter().collect()), 45 | component.config.clone(), 46 | ) 47 | .await 48 | } 49 | Command::StartProvider(prov) => { 50 | trace!(command = ?prov, "Handling start provider command"); 51 | // Order here is intentional to prevent scalers from overwriting managed annotations 52 | let mut annotations = prov.annotations.clone(); 53 | insert_managed_annotations(&mut annotations, &prov.model_name); 54 | self.client 55 | .start_provider( 56 | &prov.host_id, 57 | &prov.reference, 58 | &prov.provider_id, 59 | Some(annotations.into_iter().collect()), 60 | prov.config.clone(), 61 | ) 62 | .await 63 | } 64 | Command::StopProvider(prov) => { 65 | trace!(command = ?prov, "Handling stop provider command"); 66 | // Order here is intentional to prevent scalers from overwriting managed annotations 67 | let mut annotations = prov.annotations.clone(); 68 | insert_managed_annotations(&mut annotations, &prov.model_name); 69 | self.client 70 | .stop_provider(&prov.host_id, &prov.provider_id) 71 | .await 72 | } 73 | Command::PutLink(ld) => { 74 | trace!(command = ?ld, "Handling put linkdef command"); 75 | // TODO(thomastaylor312): We should probably change ScopedMessage to allow us `pub` 76 | // access to the inner type so we don't have to clone, but no need to worry for now 77 | self.client.put_link(ld.clone().try_into()?).await 78 | } 79 | Command::DeleteLink(ld) => { 80 | trace!(command = ?ld, "Handling delete linkdef command"); 81 | self.client 82 | .delete_link( 83 | &ld.source_id, 84 | &ld.link_name, 85 | &ld.wit_namespace, 86 | &ld.wit_package, 87 | ) 88 | .await 89 | } 90 | Command::PutConfig(put_config) => { 91 | trace!(command = ?put_config, "Handling put config command"); 92 | self.client 93 | .put_config(&put_config.config_name, put_config.config.clone()) 94 | .await 95 | } 96 | Command::DeleteConfig(delete_config) => { 97 | trace!(command = ?delete_config, "Handling delete config command"); 98 | self.client.delete_config(&delete_config.config_name).await 99 | } 100 | } 101 | .map_err(|e| anyhow::anyhow!("{e:?}")); 102 | 103 | match res { 104 | Ok(ack) if !ack.succeeded() => { 105 | message.nack().await; 106 | Err(WorkError::Other( 107 | anyhow::anyhow!("{}", ack.message()).into(), 108 | )) 109 | } 110 | Ok(_) => message.ack().await.map_err(WorkError::from), 111 | Err(e) => { 112 | message.nack().await; 113 | Err(WorkError::Other(e.into())) 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /wit/wadm/types.wit: -------------------------------------------------------------------------------- 1 | package wasmcloud:wadm@0.2.0; 2 | 3 | interface types { 4 | record model-summary { 5 | name: string, 6 | version: string, 7 | description: option, 8 | deployed-version: option, 9 | status: status-type, 10 | status-message: option 11 | } 12 | 13 | record version-info { 14 | version: string, 15 | deployed: bool 16 | } 17 | 18 | record status-update { 19 | app: string, 20 | status: status 21 | } 22 | 23 | record status { 24 | version: string, 25 | info: status-info, 26 | components: list 27 | } 28 | 29 | record component-status { 30 | name: string, 31 | component-type: string, 32 | info: status-info, 33 | traits: list 34 | } 35 | 36 | record trait-status { 37 | trait-type: string, 38 | info: status-info 39 | } 40 | 41 | record status-info { 42 | status-type: status-type, 43 | message: string 44 | } 45 | 46 | enum put-result { 47 | error, 48 | created, 49 | new-version 50 | } 51 | 52 | enum get-result { 53 | error, 54 | success, 55 | not-found 56 | } 57 | 58 | enum status-result { 59 | error, 60 | ok, 61 | not-found 62 | } 63 | 64 | enum delete-result { 65 | deleted, 66 | error, 67 | noop 68 | } 69 | 70 | enum status-type { 71 | undeployed, 72 | reconciling, 73 | deployed, 74 | failed, 75 | waiting, 76 | unhealthy 77 | } 78 | 79 | enum deploy-result { 80 | error, 81 | acknowledged, 82 | not-found 83 | } 84 | 85 | // The overall structure of an OAM manifest. 86 | record oam-manifest { 87 | api-version: string, 88 | kind: string, 89 | metadata: metadata, 90 | spec: specification, 91 | } 92 | 93 | // Metadata describing the manifest 94 | record metadata { 95 | name: string, 96 | annotations: list>, 97 | labels: list>, 98 | } 99 | 100 | // The specification for this manifest 101 | record specification { 102 | components: list, 103 | policies: list 104 | } 105 | 106 | // A component definition 107 | record component { 108 | name: string, 109 | properties: properties, 110 | traits: option>, 111 | } 112 | 113 | // Properties that can be defined for a component 114 | variant properties { 115 | component(component-properties), 116 | capability(capability-properties), 117 | } 118 | 119 | // Properties for a component 120 | record component-properties { 121 | image: option, 122 | application: option, 123 | id: option, 124 | config: list, 125 | secrets: list, 126 | } 127 | 128 | // Properties for a capability 129 | record capability-properties { 130 | image: option, 131 | application: option, 132 | id: option, 133 | config: list, 134 | secrets: list, 135 | } 136 | 137 | // A policy definition 138 | record policy { 139 | name: string, 140 | properties: list>, 141 | %type: string, 142 | } 143 | 144 | // A trait definition 145 | record trait { 146 | trait-type: string, 147 | properties: trait-property, 148 | } 149 | 150 | // Properties for defining traits 151 | variant trait-property { 152 | link(link-property), 153 | spreadscaler(spreadscaler-property), 154 | custom(string), 155 | } 156 | 157 | // Properties for links 158 | record link-property { 159 | namespace: string, 160 | %package: string, 161 | interfaces: list, 162 | source: option, 163 | target: target-config, 164 | name: option, 165 | } 166 | 167 | // Configuration definition 168 | record config-definition { 169 | config: list, 170 | secrets: list, 171 | } 172 | 173 | // Configuration properties 174 | record config-property { 175 | name: string, 176 | properties: option>>, 177 | } 178 | 179 | // Secret properties 180 | record secret-property { 181 | name: string, 182 | properties: secret-source-property, 183 | } 184 | 185 | // Secret source properties 186 | record secret-source-property { 187 | policy: string, 188 | key: string, 189 | field: option, 190 | version: option, 191 | } 192 | 193 | // Shared application component properties 194 | record shared-application-component-properties { 195 | name: string, 196 | component: string 197 | } 198 | 199 | // Target configuration 200 | record target-config { 201 | name: string, 202 | config: list, 203 | secrets: list, 204 | } 205 | 206 | // Properties for spread scalers 207 | record spreadscaler-property { 208 | instances: u32, 209 | spread: list, 210 | } 211 | 212 | // Configuration for various spreading requirements 213 | record spread { 214 | name: string, 215 | requirements: list>, 216 | weight: option, 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /crates/wadm-types/wit/deps/wadm/types.wit: -------------------------------------------------------------------------------- 1 | package wasmcloud:wadm@0.2.0; 2 | 3 | interface types { 4 | record model-summary { 5 | name: string, 6 | version: string, 7 | description: option, 8 | deployed-version: option, 9 | status: status-type, 10 | status-message: option 11 | } 12 | 13 | record version-info { 14 | version: string, 15 | deployed: bool 16 | } 17 | 18 | record status-update { 19 | app: string, 20 | status: status 21 | } 22 | 23 | record status { 24 | version: string, 25 | info: status-info, 26 | components: list 27 | } 28 | 29 | record component-status { 30 | name: string, 31 | component-type: string, 32 | info: status-info, 33 | traits: list 34 | } 35 | 36 | record trait-status { 37 | trait-type: string, 38 | info: status-info 39 | } 40 | 41 | record status-info { 42 | status-type: status-type, 43 | message: string 44 | } 45 | 46 | enum put-result { 47 | error, 48 | created, 49 | new-version 50 | } 51 | 52 | enum get-result { 53 | error, 54 | success, 55 | not-found 56 | } 57 | 58 | enum status-result { 59 | error, 60 | ok, 61 | not-found 62 | } 63 | 64 | enum delete-result { 65 | deleted, 66 | error, 67 | noop 68 | } 69 | 70 | enum status-type { 71 | undeployed, 72 | reconciling, 73 | deployed, 74 | failed, 75 | waiting, 76 | unhealthy 77 | } 78 | 79 | enum deploy-result { 80 | error, 81 | acknowledged, 82 | not-found 83 | } 84 | 85 | // The overall structure of an OAM manifest. 86 | record oam-manifest { 87 | api-version: string, 88 | kind: string, 89 | metadata: metadata, 90 | spec: specification, 91 | } 92 | 93 | // Metadata describing the manifest 94 | record metadata { 95 | name: string, 96 | annotations: list>, 97 | labels: list>, 98 | } 99 | 100 | // The specification for this manifest 101 | record specification { 102 | components: list, 103 | policies: list 104 | } 105 | 106 | // A component definition 107 | record component { 108 | name: string, 109 | properties: properties, 110 | traits: option>, 111 | } 112 | 113 | // Properties that can be defined for a component 114 | variant properties { 115 | component(component-properties), 116 | capability(capability-properties), 117 | } 118 | 119 | // Properties for a component 120 | record component-properties { 121 | image: option, 122 | application: option, 123 | id: option, 124 | config: list, 125 | secrets: list, 126 | } 127 | 128 | // Properties for a capability 129 | record capability-properties { 130 | image: option, 131 | application: option, 132 | id: option, 133 | config: list, 134 | secrets: list, 135 | } 136 | 137 | // A policy definition 138 | record policy { 139 | name: string, 140 | properties: list>, 141 | %type: string, 142 | } 143 | 144 | // A trait definition 145 | record trait { 146 | trait-type: string, 147 | properties: trait-property, 148 | } 149 | 150 | // Properties for defining traits 151 | variant trait-property { 152 | link(link-property), 153 | spreadscaler(spreadscaler-property), 154 | custom(string), 155 | } 156 | 157 | // Properties for links 158 | record link-property { 159 | namespace: string, 160 | %package: string, 161 | interfaces: list, 162 | source: option, 163 | target: target-config, 164 | name: option, 165 | } 166 | 167 | // Configuration definition 168 | record config-definition { 169 | config: list, 170 | secrets: list, 171 | } 172 | 173 | // Configuration properties 174 | record config-property { 175 | name: string, 176 | properties: option>>, 177 | } 178 | 179 | // Secret properties 180 | record secret-property { 181 | name: string, 182 | properties: secret-source-property, 183 | } 184 | 185 | // Secret source properties 186 | record secret-source-property { 187 | policy: string, 188 | key: string, 189 | field: option, 190 | version: option, 191 | } 192 | 193 | // Shared application component properties 194 | record shared-application-component-properties { 195 | name: string, 196 | component: string 197 | } 198 | 199 | // Target configuration 200 | record target-config { 201 | name: string, 202 | config: list, 203 | secrets: list, 204 | } 205 | 206 | // Properties for spread scalers 207 | record spreadscaler-property { 208 | instances: u32, 209 | spread: list, 210 | } 211 | 212 | // Configuration for various spreading requirements 213 | record spread { 214 | name: string, 215 | requirements: list>, 216 | weight: option, 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /tests/validation.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context as _, Result}; 2 | 3 | use wadm_types::validation::{validate_manifest_file, ValidationFailureLevel, ValidationOutput}; 4 | 5 | /// Ensure that valid YAML manifests are valid 6 | #[tokio::test] 7 | async fn validate_pass() -> Result<()> { 8 | assert!( 9 | validate_manifest_file("./tests/fixtures/manifests/simple.wadm.yaml") 10 | .await? 11 | .1 12 | .valid() 13 | ); 14 | Ok(()) 15 | } 16 | 17 | /// Ensure that we can detect dangling links 18 | #[tokio::test] 19 | async fn validate_dangling_links() -> Result<()> { 20 | let (_manifest, failures) = 21 | validate_manifest_file("./tests/fixtures/manifests/dangling-link.wadm.yaml") 22 | .await 23 | .context("failed to validate manifest")?; 24 | assert!( 25 | !failures.is_empty() 26 | && failures 27 | .iter() 28 | .all(|f| f.level == ValidationFailureLevel::Warning 29 | || f.level == ValidationFailureLevel::Error), 30 | "failures present, all warnings or errors" 31 | ); 32 | assert!(!failures.valid(), "manifest should not be valid"); 33 | Ok(()) 34 | } 35 | 36 | /// Ensure that we can detect misnamed interfaces 37 | #[tokio::test] 38 | async fn validate_misnamed_interface() -> Result<()> { 39 | let (_manifest, failures) = 40 | validate_manifest_file("./tests/fixtures/manifests/misnamed-interface.wadm.yaml") 41 | .await 42 | .context("failed to validate manifest")?; 43 | assert!( 44 | !failures.is_empty() 45 | && failures 46 | .iter() 47 | .all(|f| f.level == ValidationFailureLevel::Warning), 48 | "failures present, all warnings" 49 | ); 50 | assert!( 51 | failures.valid(), 52 | "manifest should be valid (misnamed interface w/ right namespace & package is probably a bug but might be intentional)" 53 | ); 54 | Ok(()) 55 | } 56 | 57 | /// Ensure that we can detect unknown packages under known namespaces 58 | #[tokio::test] 59 | async fn validate_unknown_package() -> Result<()> { 60 | let (_manifest, failures) = 61 | validate_manifest_file("./tests/fixtures/manifests/unknown-package.wadm.yaml") 62 | .await 63 | .context("failed to validate manifest")?; 64 | assert!( 65 | !failures.is_empty() 66 | && failures 67 | .iter() 68 | .all(|f| f.level == ValidationFailureLevel::Warning), 69 | "failures present, all errors" 70 | ); 71 | assert!( 72 | failures.valid(), 73 | "manifest should be valid (unknown package under a known interface is a warning)" 74 | ); 75 | Ok(()) 76 | } 77 | 78 | /// Ensure that we allow through custom interface 79 | #[tokio::test] 80 | async fn validate_custom_interface() -> Result<()> { 81 | let (_manifest, failures) = 82 | validate_manifest_file("./tests/fixtures/manifests/custom-interface.wadm.yaml") 83 | .await 84 | .context("failed to validate manifest")?; 85 | assert!(failures.is_empty(), "no failures"); 86 | assert!( 87 | failures.valid(), 88 | "manifest is valid (custom namespace is default-allowed)" 89 | ); 90 | Ok(()) 91 | } 92 | 93 | #[tokio::test] 94 | async fn validate_bad_manifest() -> Result<()> { 95 | let result = validate_manifest_file("./tests/fixtures/manifests/made-up-block.wadm.yaml") 96 | .await 97 | .context("failed to validate manifest"); 98 | assert!(result.is_err(), "expected error"); 99 | Ok(()) 100 | } 101 | 102 | #[tokio::test] 103 | async fn validate_bad_manifest_key() -> Result<()> { 104 | let result = validate_manifest_file("./tests/fixtures/manifests/made-up-key.wadm.yaml") 105 | .await 106 | .context("failed to validate manifest"); 107 | assert!(result.is_err(), "expected error"); 108 | Ok(()) 109 | } 110 | 111 | #[tokio::test] 112 | async fn validate_policy() -> Result<()> { 113 | let (_manifest, failures) = 114 | validate_manifest_file("./tests/fixtures/manifests/policy.wadm.yaml") 115 | .await 116 | .context("failed to validate manifest")?; 117 | assert!(failures.is_empty(), "no failures"); 118 | assert!(failures.valid(), "manifest is valid"); 119 | Ok(()) 120 | } 121 | 122 | /// Ensure that we can detect duplicated link config names 123 | #[tokio::test] 124 | async fn validate_link_config_names() -> Result<()> { 125 | let (_manifest, failures) = 126 | validate_manifest_file("./tests/fixtures/manifests/duplicate_link_config_names.wadm.yaml") 127 | .await 128 | .context("failed to validate manifest")?; 129 | let expected_errors = 3; 130 | assert!( 131 | !failures.is_empty() 132 | && failures 133 | .iter() 134 | .all(|f| f.level == ValidationFailureLevel::Error) 135 | && failures.len() == expected_errors, 136 | "expected {} errors because manifest contains {} duplicated link config names, instead {} errors were found", expected_errors, expected_errors, failures.len() 137 | ); 138 | assert!( 139 | !failures.valid(), 140 | "manifest should be invalid (duplicated link config names lead to a dead loop)" 141 | ); 142 | Ok(()) 143 | } 144 | 145 | #[tokio::test] 146 | async fn validate_deprecated_configs_raw_yaml() -> Result<()> { 147 | let (_manifest, failures) = validate_manifest_file( 148 | "./tests/fixtures/manifests/deprecated-source-and-target-config.yaml", 149 | ) 150 | .await 151 | .context("failed to validate manifest")?; 152 | assert!(failures.valid(), "expected valid manifest"); 153 | assert_eq!( 154 | failures.warnings().len(), 155 | 2, 156 | "expected 2 warnings during validating manifest" 157 | ); 158 | Ok(()) 159 | } 160 | -------------------------------------------------------------------------------- /crates/wadm/src/test_util.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::{collections::HashMap, sync::Arc}; 3 | 4 | use serde::{de::DeserializeOwned, Serialize}; 5 | use tokio::sync::RwLock; 6 | use wasmcloud_control_interface::{HostInventory, Link}; 7 | use wasmcloud_secrets_types::SecretConfig; 8 | 9 | use crate::publisher::Publisher; 10 | use crate::storage::StateKind; 11 | use crate::workers::{ 12 | secret_config_from_map, Claims, ClaimsSource, ConfigSource, InventorySource, LinkSource, 13 | SecretSource, 14 | }; 15 | 16 | fn generate_key(lattice_id: &str) -> String { 17 | format!("{}_{lattice_id}", T::KIND) 18 | } 19 | 20 | /// A [`Store`] implementation for use in testing 21 | #[derive(Default)] 22 | pub struct TestStore { 23 | pub inner: tokio::sync::RwLock>>, 24 | } 25 | 26 | #[async_trait::async_trait] 27 | impl crate::storage::ReadStore for TestStore { 28 | type Error = Infallible; 29 | 30 | async fn get(&self, lattice_id: &str, id: &str) -> Result, Self::Error> 31 | where 32 | T: DeserializeOwned + StateKind, 33 | { 34 | let key = generate_key::(lattice_id); 35 | let mut all: HashMap = self 36 | .inner 37 | .read() 38 | .await 39 | .get(&key) 40 | .map(|raw| serde_json::from_slice(raw).unwrap()) 41 | .unwrap_or_default(); 42 | // T isn't clone, so I can't use get 43 | Ok(all.remove(id)) 44 | } 45 | 46 | async fn list(&self, lattice_id: &str) -> Result, Self::Error> 47 | where 48 | T: DeserializeOwned + StateKind, 49 | { 50 | let key = generate_key::(lattice_id); 51 | Ok(self 52 | .inner 53 | .read() 54 | .await 55 | .get(&key) 56 | .map(|raw| serde_json::from_slice(raw).unwrap()) 57 | .unwrap_or_default()) 58 | } 59 | } 60 | 61 | #[async_trait::async_trait] 62 | impl crate::storage::Store for TestStore { 63 | async fn store_many(&self, lattice_id: &str, data: D) -> Result<(), Self::Error> 64 | where 65 | T: Serialize + DeserializeOwned + StateKind + Send + Sync + Clone, 66 | D: IntoIterator + Send, 67 | { 68 | let key = generate_key::(lattice_id); 69 | let mut all: HashMap = self 70 | .inner 71 | .read() 72 | .await 73 | .get(&key) 74 | .map(|raw| serde_json::from_slice(raw).unwrap()) 75 | .unwrap_or_default(); 76 | all.extend(data); 77 | self.inner 78 | .write() 79 | .await 80 | .insert(key, serde_json::to_vec(&all).unwrap()); 81 | Ok(()) 82 | } 83 | 84 | async fn delete_many(&self, lattice_id: &str, data: D) -> Result<(), Self::Error> 85 | where 86 | T: Serialize + DeserializeOwned + StateKind + Send + Sync, 87 | D: IntoIterator + Send, 88 | K: AsRef, 89 | { 90 | let key = generate_key::(lattice_id); 91 | let mut all: HashMap = self 92 | .inner 93 | .read() 94 | .await 95 | .get(&key) 96 | .map(|raw| serde_json::from_slice(raw).unwrap()) 97 | .unwrap_or_default(); 98 | for k in data.into_iter() { 99 | all.remove(k.as_ref()); 100 | } 101 | self.inner 102 | .write() 103 | .await 104 | .insert(key, serde_json::to_vec(&all).unwrap()); 105 | Ok(()) 106 | } 107 | } 108 | 109 | #[derive(Clone, Default, Debug)] 110 | /// A test "lattice source" for use with testing 111 | pub struct TestLatticeSource { 112 | pub claims: HashMap, 113 | pub inventory: Arc>>, 114 | pub links: Vec, 115 | pub config: HashMap>, 116 | } 117 | 118 | #[async_trait::async_trait] 119 | impl ClaimsSource for TestLatticeSource { 120 | async fn get_claims(&self) -> anyhow::Result> { 121 | Ok(self.claims.clone()) 122 | } 123 | } 124 | 125 | #[async_trait::async_trait] 126 | impl InventorySource for TestLatticeSource { 127 | async fn get_inventory(&self, host_id: &str) -> anyhow::Result { 128 | Ok(self.inventory.read().await.get(host_id).cloned().unwrap()) 129 | } 130 | } 131 | 132 | #[async_trait::async_trait] 133 | impl LinkSource for TestLatticeSource { 134 | async fn get_links(&self) -> anyhow::Result> { 135 | Ok(self.links.clone()) 136 | } 137 | } 138 | 139 | #[async_trait::async_trait] 140 | impl ConfigSource for TestLatticeSource { 141 | async fn get_config(&self, name: &str) -> anyhow::Result>> { 142 | Ok(self.config.get(name).cloned()) 143 | } 144 | } 145 | 146 | #[async_trait::async_trait] 147 | impl SecretSource for TestLatticeSource { 148 | async fn get_secret(&self, name: &str) -> anyhow::Result> { 149 | let secret_config = self 150 | .get_config(format!("secret_{name}").as_str()) 151 | .await 152 | .map_err(|e| anyhow::anyhow!("{e:?}"))?; 153 | 154 | secret_config.map(secret_config_from_map).transpose() 155 | } 156 | } 157 | 158 | /// A publisher that does nothing 159 | #[derive(Clone, Default)] 160 | pub struct NoopPublisher; 161 | 162 | #[async_trait::async_trait] 163 | impl Publisher for NoopPublisher { 164 | async fn publish(&self, _: Vec, _: Option<&str>) -> anyhow::Result<()> { 165 | Ok(()) 166 | } 167 | } 168 | 169 | /// A publisher that records all data sent to it (as the given type deserialized from JSON) 170 | pub struct RecorderPublisher { 171 | pub received: Arc>>, 172 | } 173 | 174 | #[async_trait::async_trait] 175 | impl Publisher for RecorderPublisher { 176 | async fn publish(&self, data: Vec, _: Option<&str>) -> anyhow::Result<()> { 177 | let data: T = serde_json::from_slice(&data)?; 178 | self.received.write().await.push(data); 179 | Ok(()) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /crates/wadm/src/consumers/mod.rs: -------------------------------------------------------------------------------- 1 | //! Contains implementions of durable consumers of events that automatically take a message from a 2 | //! consumer and parse it to concrete types for consumption in a scheduler 3 | 4 | use std::fmt::Debug; 5 | use std::ops::{Deref, DerefMut}; 6 | use std::time::Duration; 7 | 8 | use async_nats::jetstream::{AckKind, Message}; 9 | use async_nats::Error as NatsError; 10 | use tracing::{error, warn}; 11 | 12 | mod commands; 13 | mod events; 14 | pub mod manager; 15 | 16 | /// The default time given for a command to ack. This is longer than events due to the possible need for more processing time 17 | pub const DEFAULT_ACK_TIME: Duration = Duration::from_secs(2); 18 | 19 | pub const LATTICE_METADATA_KEY: &str = "lattice"; 20 | pub const MULTITENANT_METADATA_KEY: &str = "multitenant_prefix"; 21 | 22 | pub use commands::*; 23 | pub use events::*; 24 | 25 | /// An message that is scoped to a specific lattice. This allows to distinguish between items 26 | /// from different lattices and to handle acking. It can support any inner type. 27 | /// 28 | /// This type implements AsRef/Mut and Deref/Mut [`Event`] for convenience so it can still be used 29 | /// as a normal event 30 | /// 31 | /// When this struct is dropped, it will automatically NAK the event unless an ack has already been 32 | /// sent 33 | pub struct ScopedMessage { 34 | /// The id of the lattice to which this event belongs 35 | pub lattice_id: String, 36 | 37 | pub(crate) inner: T, 38 | // Wrapped in an option so we only do it once 39 | pub(crate) acker: Option, 40 | } 41 | 42 | impl ScopedMessage { 43 | /// Acks this Event. This should be called when all work related to this event has been 44 | /// completed. If this is called before work is done (e.g. like sending a command), instability 45 | /// could occur. Calling this function again (or after nacking) is a noop. 46 | /// 47 | /// This function will only error after it has tried up to 3 times to ack the request. If it 48 | /// doesn't receive a response after those 3 times, this will return an error. 49 | pub async fn ack(&mut self) -> Result<(), NatsError> { 50 | // We want to double ack so we are sure that the server has marked this task as done 51 | if let Some(msg) = self.acker.take() { 52 | // Starting at 1 for humans/logging 53 | let mut retry_count = 1; 54 | loop { 55 | match msg.double_ack().await { 56 | Ok(_) => break Ok(()), 57 | Err(e) if retry_count == 3 => break Err(e), 58 | Err(e) => { 59 | warn!(error = %e, %retry_count, "Failed to receive ack response, will retry"); 60 | retry_count += 1; 61 | tokio::time::sleep(Duration::from_millis(100)).await 62 | } 63 | } 64 | } 65 | } else { 66 | warn!("Ack has already been sent"); 67 | Ok(()) 68 | } 69 | } 70 | 71 | /// This is a function for advanced use. If you'd like to send a specific Ack signal back to the 72 | /// server, use this function 73 | /// 74 | /// Returns the underlying NATS error, if any. If an error occurs, you can try to ack again 75 | pub async fn custom_ack(&mut self, kind: AckKind) -> Result<(), NatsError> { 76 | if let Some(msg) = self.acker.take() { 77 | if let Err(e) = msg.ack_with(kind).await { 78 | // Put it back so it can be called again 79 | self.acker = Some(msg); 80 | Err(e) 81 | } else { 82 | Ok(()) 83 | } 84 | } else { 85 | warn!("Nack has already been sent"); 86 | Ok(()) 87 | } 88 | } 89 | 90 | /// Nacks this Event. This should be called if there was an error when processing. By default, 91 | /// this is called when a [`ScopedMessage`] is dropped. Calling this again is a noop. 92 | /// 93 | /// This method doesn't have an error because if you nack, it means the message needs to be 94 | /// rehandled, so the ack wait will eventually expire 95 | pub async fn nack(&mut self) { 96 | if let Err(e) = self.custom_ack(AckKind::Nak(None)).await { 97 | error!(error = %e, "Error when nacking message"); 98 | self.acker = None; 99 | } 100 | } 101 | } 102 | 103 | impl Drop for ScopedMessage { 104 | fn drop(&mut self) { 105 | // self.nack escapes current lifetime, so just manually take the message 106 | if let Some(msg) = self.acker.take() { 107 | if let Ok(handle) = tokio::runtime::Handle::try_current() { 108 | handle.spawn(async move { 109 | if let Err(e) = msg.ack_with(AckKind::Nak(None)).await { 110 | warn!(error = %e, "Error when sending nack during drop") 111 | } 112 | }); 113 | } else { 114 | warn!("Couldn't find async runtime to send nack during drop") 115 | } 116 | } 117 | } 118 | } 119 | 120 | impl AsRef for ScopedMessage { 121 | fn as_ref(&self) -> &T { 122 | &self.inner 123 | } 124 | } 125 | 126 | impl AsMut for ScopedMessage { 127 | fn as_mut(&mut self) -> &mut T { 128 | &mut self.inner 129 | } 130 | } 131 | 132 | impl Deref for ScopedMessage { 133 | type Target = T; 134 | 135 | fn deref(&self) -> &Self::Target { 136 | &self.inner 137 | } 138 | } 139 | 140 | impl DerefMut for ScopedMessage { 141 | fn deref_mut(&mut self) -> &mut Self::Target { 142 | &mut self.inner 143 | } 144 | } 145 | 146 | impl Debug for ScopedMessage { 147 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 148 | f.debug_struct("ScopedMessage") 149 | .field("lattice_id", &self.lattice_id) 150 | .field("inner", &self.inner) 151 | .finish() 152 | } 153 | } 154 | 155 | /// A helper trait to allow for constructing any consumer 156 | #[async_trait::async_trait] 157 | pub trait CreateConsumer { 158 | type Output: Unpin; 159 | 160 | /// Create a type of the specified `Output` 161 | async fn create( 162 | stream: async_nats::jetstream::stream::Stream, 163 | topic: &str, 164 | lattice_id: &str, 165 | multitenant_prefix: Option<&str>, 166 | ) -> Result; 167 | } 168 | -------------------------------------------------------------------------------- /crates/wadm/src/consumers/commands.rs: -------------------------------------------------------------------------------- 1 | //! A module for creating and consuming a stream of commands from NATS 2 | 3 | use std::collections::HashMap; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use async_nats::{ 8 | jetstream::{ 9 | consumer::pull::{Config as PullConfig, Stream as MessageStream}, 10 | stream::Stream as JsStream, 11 | }, 12 | Error as NatsError, 13 | }; 14 | use futures::{Stream, TryStreamExt}; 15 | use tracing::{error, warn}; 16 | 17 | use super::{CreateConsumer, ScopedMessage, LATTICE_METADATA_KEY, MULTITENANT_METADATA_KEY}; 18 | use crate::commands::*; 19 | 20 | /// The name of the durable NATS stream and consumer that contains incoming lattice events 21 | pub const COMMANDS_CONSUMER_PREFIX: &str = "wadm_commands"; 22 | 23 | /// A stream of all commands in a lattice, consumed from a durable NATS stream and consumer 24 | pub struct CommandConsumer { 25 | stream: MessageStream, 26 | lattice_id: String, 27 | } 28 | 29 | impl CommandConsumer { 30 | /// Creates a new command consumer, returning an error if unable to create or access the durable 31 | /// consumer on the given stream. 32 | /// 33 | /// The `topic` param should be a valid topic where lattice events are expected to be sent and 34 | /// should match the given lattice ID. An error will be returned if the given lattice ID is not 35 | /// contained in the topic 36 | pub async fn new( 37 | stream: JsStream, 38 | topic: &str, 39 | lattice_id: &str, 40 | multitenant_prefix: Option<&str>, 41 | ) -> Result { 42 | if !topic.contains(lattice_id) { 43 | return Err(format!("Topic {topic} does not match for lattice ID {lattice_id}").into()); 44 | } 45 | 46 | let (consumer_name, metadata) = if let Some(prefix) = multitenant_prefix { 47 | ( 48 | format!("{COMMANDS_CONSUMER_PREFIX}-{lattice_id}_{prefix}"), 49 | HashMap::from([ 50 | (LATTICE_METADATA_KEY.to_string(), lattice_id.to_string()), 51 | (MULTITENANT_METADATA_KEY.to_string(), prefix.to_string()), 52 | ]), 53 | ) 54 | } else { 55 | ( 56 | format!("{COMMANDS_CONSUMER_PREFIX}-{lattice_id}"), 57 | HashMap::from([(LATTICE_METADATA_KEY.to_string(), lattice_id.to_string())]), 58 | ) 59 | }; 60 | let consumer = stream 61 | .get_or_create_consumer( 62 | &consumer_name, 63 | PullConfig { 64 | durable_name: Some(consumer_name.clone()), 65 | name: Some(consumer_name.clone()), 66 | description: Some(format!( 67 | "Durable wadm commands consumer for lattice {lattice_id}" 68 | )), 69 | ack_policy: async_nats::jetstream::consumer::AckPolicy::Explicit, 70 | ack_wait: super::DEFAULT_ACK_TIME, 71 | max_deliver: 3, 72 | deliver_policy: async_nats::jetstream::consumer::DeliverPolicy::All, 73 | filter_subject: topic.to_owned(), 74 | metadata, 75 | ..Default::default() 76 | }, 77 | ) 78 | .await?; 79 | let messages = consumer 80 | .stream() 81 | .max_messages_per_batch(1) 82 | .messages() 83 | .await?; 84 | Ok(CommandConsumer { 85 | stream: messages, 86 | lattice_id: lattice_id.to_owned(), 87 | }) 88 | } 89 | } 90 | 91 | impl Stream for CommandConsumer { 92 | type Item = Result, NatsError>; 93 | 94 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 95 | match self.stream.try_poll_next_unpin(cx) { 96 | Poll::Ready(None) => Poll::Ready(None), 97 | Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(Box::new(e)))), 98 | Poll::Ready(Some(Ok(msg))) => { 99 | // Convert to our event type, skipping if we can't do it (and looping around to 100 | // try the next poll) 101 | let cmd = match serde_json::from_slice(&msg.payload) { 102 | Ok(cmd) => cmd, 103 | Err(e) => { 104 | warn!(error = ?e, "Unable to decode as command. Skipping message"); 105 | // This is slightly janky, but rather than having to store and poll the 106 | // future (which gets a little gnarly), just pass the message onto a 107 | // spawned thread which wakes up the thread when it is done acking. 108 | let waker = cx.waker().clone(); 109 | // NOTE: If we are already in a stream impl, we should be able to spawn 110 | // without worrying. A panic isn't the worst here if for some reason we 111 | // can't as it means we can't ack the message and we'll be stuck waiting 112 | // for it to deliver again until it fails 113 | tokio::spawn(async move { 114 | if let Err(e) = msg.ack().await { 115 | error!(error = %e, "Error when trying to ack skipped message, message will be redelivered") 116 | } 117 | waker.wake(); 118 | }); 119 | // Return a poll pending. It will then wake up and try again once it has acked 120 | return Poll::Pending; 121 | } 122 | }; 123 | // NOTE(thomastaylor312): Ideally we'd consume `msg.payload` above with a 124 | // `Cursor` and `from_reader` and then manually reconstruct the acking using the 125 | // message context, but I didn't want to waste time optimizing yet 126 | Poll::Ready(Some(Ok(ScopedMessage { 127 | lattice_id: self.lattice_id.clone(), 128 | inner: cmd, 129 | acker: Some(msg), 130 | }))) 131 | } 132 | Poll::Pending => Poll::Pending, 133 | } 134 | } 135 | } 136 | 137 | #[async_trait::async_trait] 138 | impl CreateConsumer for CommandConsumer { 139 | type Output = CommandConsumer; 140 | 141 | async fn create( 142 | stream: async_nats::jetstream::stream::Stream, 143 | topic: &str, 144 | lattice_id: &str, 145 | multitenant_prefix: Option<&str>, 146 | ) -> Result { 147 | CommandConsumer::new(stream, topic, lattice_id, multitenant_prefix).await 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /crates/wadm/src/observer.rs: -------------------------------------------------------------------------------- 1 | //! Types for observing a nats cluster for new lattices 2 | 3 | use async_nats::Subscriber; 4 | use futures::{stream::SelectAll, StreamExt, TryFutureExt}; 5 | use tracing::{debug, error, instrument, trace, warn}; 6 | 7 | use crate::{ 8 | consumers::{ 9 | manager::{ConsumerManager, WorkerCreator}, 10 | CommandConsumer, EventConsumer, 11 | }, 12 | events::{EventType, HostHeartbeat, HostStarted, ManifestPublished}, 13 | nats_utils::LatticeIdParser, 14 | storage::{nats_kv::NatsKvStore, reaper::Reaper, Store}, 15 | DEFAULT_COMMANDS_TOPIC, DEFAULT_WADM_EVENT_CONSUMER_TOPIC, 16 | }; 17 | 18 | use super::{CommandWorkerCreator, EventWorkerCreator}; 19 | 20 | pub(crate) struct Observer { 21 | pub(crate) parser: LatticeIdParser, 22 | pub(crate) command_manager: ConsumerManager, 23 | pub(crate) event_manager: ConsumerManager, 24 | pub(crate) client: async_nats::Client, 25 | pub(crate) reaper: Reaper, 26 | pub(crate) event_worker_creator: EventWorkerCreator, 27 | pub(crate) command_worker_creator: CommandWorkerCreator, 28 | } 29 | 30 | impl Observer 31 | where 32 | StateStore: Store + Send + Sync + Clone + 'static, 33 | { 34 | /// Watches the given topic (with wildcards) for wasmbus events. If it finds a lattice that it 35 | /// isn't managing, it will start managing it immediately 36 | /// 37 | /// If this errors, it should be considered fatal 38 | #[instrument(level = "info", skip(self))] 39 | pub(crate) async fn observe(mut self, subscribe_topics: Vec) -> anyhow::Result<()> { 40 | let mut sub = get_subscriber(&self.client, subscribe_topics.clone()).await?; 41 | loop { 42 | match sub.next().await { 43 | Some(msg) => { 44 | if !is_event_we_care_about(&msg.payload) { 45 | continue; 46 | } 47 | 48 | let Some(lattice_info) = self.parser.parse(&msg.subject) else { 49 | trace!(subject = %msg.subject, "Found non-matching lattice subject"); 50 | continue; 51 | }; 52 | let lattice_id = lattice_info.lattice_id(); 53 | let multitenant_prefix = lattice_info.multitenant_prefix(); 54 | let event_subject = lattice_info.event_subject(); 55 | 56 | // Create the reaper for this lattice. This operation returns early if it is 57 | // already running 58 | self.reaper.observe(lattice_id); 59 | 60 | let command_topic = DEFAULT_COMMANDS_TOPIC.replace('*', lattice_id); 61 | let events_topic = DEFAULT_WADM_EVENT_CONSUMER_TOPIC.replace('*', lattice_id); 62 | let needs_command = !self.command_manager.has_consumer(&command_topic).await; 63 | let needs_event = !self.event_manager.has_consumer(&events_topic).await; 64 | if needs_command { 65 | debug!(%lattice_id, subject = %event_subject, mapped_subject = %command_topic, "Found unmonitored lattice, adding command consumer"); 66 | let worker = match self 67 | .command_worker_creator 68 | .create(lattice_id, multitenant_prefix) 69 | .await 70 | { 71 | Ok(w) => w, 72 | Err(e) => { 73 | error!(error = %e, %lattice_id, "Couldn't construct worker for command consumer. Will retry on next heartbeat"); 74 | continue; 75 | } 76 | }; 77 | self.command_manager 78 | .add_for_lattice(&command_topic, lattice_id, multitenant_prefix, worker) 79 | .await 80 | .unwrap_or_else(|e| { 81 | error!(error = %e, %lattice_id, "Couldn't add command consumer. Will retry on next heartbeat"); 82 | }) 83 | } 84 | if needs_event { 85 | debug!(%lattice_id, subject = %event_subject, mapped_subject = %events_topic, "Found unmonitored lattice, adding event consumer"); 86 | let worker = match self 87 | .event_worker_creator 88 | .create(lattice_id, multitenant_prefix) 89 | .await 90 | { 91 | Ok(w) => w, 92 | Err(e) => { 93 | error!(error = %e, %lattice_id, "Couldn't construct worker for event consumer. Will retry on next heartbeat"); 94 | continue; 95 | } 96 | }; 97 | self.event_manager 98 | .add_for_lattice(&events_topic, lattice_id, multitenant_prefix, worker) 99 | .await 100 | .unwrap_or_else(|e| { 101 | error!(error = %e, %lattice_id, "Couldn't add event consumer. Will retry on next heartbeat"); 102 | }) 103 | } 104 | } 105 | None => { 106 | warn!("Observer subscriber hang up. Attempting to restart"); 107 | sub = get_subscriber(&self.client, subscribe_topics.clone()).await?; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | // This is a stupid hacky function to check that this is a host started, host heartbeat, or 115 | // manifest_published event without actually parsing 116 | fn is_event_we_care_about(data: &[u8]) -> bool { 117 | let string_data = match std::str::from_utf8(data) { 118 | Ok(s) => s, 119 | Err(_) => return false, 120 | }; 121 | 122 | string_data.contains(HostStarted::TYPE) 123 | || string_data.contains(HostHeartbeat::TYPE) 124 | || string_data.contains(ManifestPublished::TYPE) 125 | } 126 | 127 | async fn get_subscriber( 128 | client: &async_nats::Client, 129 | subscribe_topics: Vec, 130 | ) -> anyhow::Result> { 131 | let futs = subscribe_topics 132 | .clone() 133 | .into_iter() 134 | .map(|t| client.subscribe(t).map_err(|e| anyhow::anyhow!("{e:?}"))); 135 | let subs: Vec = futures::future::join_all(futs) 136 | .await 137 | .into_iter() 138 | .collect::>()?; 139 | Ok(futures::stream::select_all(subs)) 140 | } 141 | -------------------------------------------------------------------------------- /crates/wadm/src/storage/snapshot.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use tokio::sync::RwLock; 5 | use tracing::debug; 6 | use wasmcloud_control_interface::Link; 7 | use wasmcloud_secrets_types::SecretConfig; 8 | 9 | use crate::storage::{Component, Host, Provider, ReadStore, StateKind}; 10 | use crate::workers::{ConfigSource, LinkSource, SecretSource}; 11 | 12 | // NOTE(thomastaylor312): This type is real ugly and we should probably find a better way to 13 | // structure the ReadStore trait so it doesn't have the generic T we have to work around here. This 14 | // is essentially a map of "state kind" -> map of ID to partially serialized state. I did try to 15 | // implement some sort of getter trait but it has to be generic across T 16 | type InMemoryData = HashMap>; 17 | 18 | /// A store and claims/links source implementation that contains a static snapshot of the data that 19 | /// can be refreshed periodically. Please note that this is scoped to a specific lattice ID and 20 | /// should be constructed separately for each lattice ID. 21 | /// 22 | /// Since configuration is fetched infrequently, and configuration might be large, we instead 23 | /// query the configuration source directly when we need it. 24 | /// 25 | /// NOTE: This is a temporary workaround until we get a proper caching store in place 26 | pub struct SnapshotStore { 27 | store: S, 28 | lattice_source: L, 29 | lattice_id: String, 30 | stored_state: Arc>, 31 | links: Arc>>, 32 | } 33 | 34 | impl Clone for SnapshotStore 35 | where 36 | S: Clone, 37 | L: Clone, 38 | { 39 | fn clone(&self) -> Self { 40 | Self { 41 | store: self.store.clone(), 42 | lattice_source: self.lattice_source.clone(), 43 | lattice_id: self.lattice_id.clone(), 44 | stored_state: self.stored_state.clone(), 45 | links: self.links.clone(), 46 | } 47 | } 48 | } 49 | 50 | impl SnapshotStore 51 | where 52 | S: ReadStore, 53 | L: LinkSource + ConfigSource + SecretSource, 54 | { 55 | /// Creates a new snapshot store that is scoped to the given lattice ID 56 | pub fn new(store: S, lattice_source: L, lattice_id: String) -> Self { 57 | Self { 58 | store, 59 | lattice_source, 60 | lattice_id, 61 | stored_state: Default::default(), 62 | links: Arc::new(RwLock::new(Vec::new())), 63 | } 64 | } 65 | 66 | /// Refreshes the snapshotted data, returning an error if it couldn't update the data 67 | pub async fn refresh(&self) -> anyhow::Result<()> { 68 | // SAFETY: All of these unwraps are safe because we _just_ deserialized from JSON 69 | let providers = self 70 | .store 71 | .list::(&self.lattice_id) 72 | .await? 73 | .into_iter() 74 | .map(|(key, val)| (key, serde_json::to_value(val).unwrap())) 75 | .collect::>(); 76 | let components = self 77 | .store 78 | .list::(&self.lattice_id) 79 | .await? 80 | .into_iter() 81 | .map(|(key, val)| (key, serde_json::to_value(val).unwrap())) 82 | .collect::>(); 83 | let hosts = self 84 | .store 85 | .list::(&self.lattice_id) 86 | .await? 87 | .into_iter() 88 | .map(|(key, val)| (key, serde_json::to_value(val).unwrap())) 89 | .collect::>(); 90 | 91 | // If we fail to get the links, that likely just means the lattice source is down, so we 92 | // just fall back on what we have cached 93 | if let Ok(links) = self.lattice_source.get_links().await { 94 | *self.links.write().await = links; 95 | } else { 96 | debug!("Failed to get links from lattice source, using cached links"); 97 | }; 98 | 99 | { 100 | let mut stored_state = self.stored_state.write().await; 101 | stored_state.insert(Provider::KIND.to_owned(), providers); 102 | stored_state.insert(Component::KIND.to_owned(), components); 103 | stored_state.insert(Host::KIND.to_owned(), hosts); 104 | } 105 | 106 | Ok(()) 107 | } 108 | } 109 | 110 | #[async_trait::async_trait] 111 | impl ReadStore for SnapshotStore 112 | where 113 | // NOTE(thomastaylor312): We need this bound so we can pass through the error type. 114 | S: ReadStore + Send + Sync, 115 | L: Send + Sync, 116 | { 117 | type Error = S::Error; 118 | 119 | // NOTE(thomastaylor312): See other note about the generic T above, but this is hardcore lolsob 120 | async fn get(&self, _lattice_id: &str, id: &str) -> Result, Self::Error> 121 | where 122 | T: serde::de::DeserializeOwned + StateKind, 123 | { 124 | Ok(self 125 | .stored_state 126 | .read() 127 | .await 128 | .get(T::KIND) 129 | .and_then(|data| { 130 | data.get(id).map(|data| { 131 | serde_json::from_value::(data.clone()).expect( 132 | "Failed to deserialize data from snapshot, this is programmer error", 133 | ) 134 | }) 135 | })) 136 | } 137 | 138 | async fn list(&self, _lattice_id: &str) -> Result, Self::Error> 139 | where 140 | T: serde::de::DeserializeOwned + StateKind, 141 | { 142 | Ok(self 143 | .stored_state 144 | .read() 145 | .await 146 | .get(T::KIND) 147 | .cloned() 148 | .unwrap_or_default() 149 | .into_iter() 150 | .map(|(key, val)| { 151 | ( 152 | key, 153 | serde_json::from_value::(val).expect( 154 | "Failed to deserialize data from snapshot, this is programmer error", 155 | ), 156 | ) 157 | }) 158 | .collect()) 159 | } 160 | } 161 | 162 | #[async_trait::async_trait] 163 | impl LinkSource for SnapshotStore 164 | where 165 | S: Send + Sync, 166 | L: Send + Sync, 167 | { 168 | async fn get_links(&self) -> anyhow::Result> { 169 | Ok(self.links.read().await.clone()) 170 | } 171 | } 172 | 173 | #[async_trait::async_trait] 174 | impl ConfigSource for SnapshotStore 175 | where 176 | S: Send + Sync, 177 | L: ConfigSource + Send + Sync, 178 | { 179 | async fn get_config(&self, name: &str) -> anyhow::Result>> { 180 | self.lattice_source.get_config(name).await 181 | } 182 | } 183 | 184 | #[async_trait::async_trait] 185 | impl SecretSource for SnapshotStore 186 | where 187 | S: Send + Sync, 188 | L: SecretSource + Send + Sync, 189 | { 190 | async fn get_secret(&self, name: &str) -> anyhow::Result> { 191 | self.lattice_source.get_secret(name).await 192 | } 193 | } 194 | --------------------------------------------------------------------------------