diff --git a/.gitignore b/.gitignore index 1a45eac5d2..c296b9f871 100644 --- a/.gitignore +++ b/.gitignore @@ -386,3 +386,8 @@ testing/e2e/logs/ # Local reports from the toolkit-pr-review-v2 skill (never committed) /REVIEW_*.md + +# Helm chart artifacts (never committed) +*.tgz +Chart.lock + diff --git a/Cargo.lock b/Cargo.lock index c61d9e72cf..3835a8c594 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1324,76 +1324,6 @@ dependencies = [ "zip 7.2.0", ] -[[package]] -name = "calculator" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "calculator-sdk", - "cf-gears-grpc-hub", - "cf-gears-toolkit", - "cf-gears-toolkit-macros", - "cf-gears-toolkit-transport-grpc", - "clap", - "inventory", - "tokio", - "tonic", - "tracing", -] - -[[package]] -name = "calculator-gateway" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "axum", - "calculator", - "calculator-sdk", - "cf-gears-toolkit", - "cf-gears-toolkit-macros", - "cf-gears-toolkit-security", - "http", - "inventory", - "serde", - "thiserror 2.0.18", - "tokio", - "tracing", - "utoipa", -] - -[[package]] -name = "calculator-sdk" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "cf-gears-system-sdks", - "cf-gears-toolkit", - "cf-gears-toolkit-security", - "cf-gears-toolkit-transport-grpc", - "prost", - "thiserror 2.0.18", - "tonic", - "tonic-prost", - "tonic-prost-build", - "tracing", -] - -[[package]] -name = "calculator_gateway_sdk" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "calculator-gateway", - "cf-gears-toolkit", - "cf-gears-toolkit-security", - "thiserror 2.0.18", - "tracing", -] - [[package]] name = "camino" version = "1.2.2" @@ -1490,6 +1420,8 @@ dependencies = [ "async-trait", "axum", "cf-api-contracts-sdk", + "cf-gears-authn-resolver", + "cf-gears-static-authn-plugin", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", "cf-gears-toolkit-contract", @@ -1497,6 +1429,8 @@ dependencies = [ "cf-gears-toolkit-macros", "cf-gears-toolkit-security", "cf-gears-toolkit-transport-grpc", + "cf-gears-types-registry", + "clap", "futures-core", "futures-util", "http", @@ -1523,11 +1457,15 @@ dependencies = [ "axum", "cf-api-contracts", "cf-api-contracts-sdk", + "cf-gears-authn-resolver", + "cf-gears-static-authn-plugin", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", "cf-gears-toolkit-contract", "cf-gears-toolkit-http", "cf-gears-toolkit-security", + "cf-gears-types-registry", + "clap", "parking_lot", "serde_json", "tokio", @@ -1796,17 +1734,23 @@ version = "0.1.38" dependencies = [ "anyhow", "async-trait", + "axum", "cf-gears-authz-resolver-sdk", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-contract", "cf-gears-toolkit-macros", + "cf-gears-toolkit-security", "cf-gears-types-registry", "cf-gears-types-registry-sdk", "inventory", "serde", "serde_json", "thiserror 2.0.18", + "tokio", + "tokio-util", "tracing", + "uuid", ] [[package]] @@ -1814,11 +1758,16 @@ name = "cf-gears-authz-resolver-sdk" version = "0.3.22" dependencies = [ "async-trait", + "axum", "cf-gears-tenant-resolver-sdk", "cf-gears-toolkit", + "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-contract", "cf-gears-toolkit-gts", + "cf-gears-toolkit-http", "cf-gears-toolkit-security", "gts", + "http", "schemars 1.2.1", "secrecy", "serde", @@ -1826,6 +1775,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", + "utoipa", "uuid", ] @@ -2289,8 +2239,6 @@ name = "cf-gears-example-server" version = "0.6.1" dependencies = [ "anyhow", - "calculator", - "calculator-gateway", "cf-chat-engine", "cf-gears-account-management", "cf-gears-api-gateway", @@ -2843,6 +2791,37 @@ dependencies = [ "uuid", ] +[[package]] +name = "cf-gears-platform-host" +version = "0.1.0" +dependencies = [ + "anyhow", + "cf-gears-account-management", + "cf-gears-api-gateway", + "cf-gears-authn-resolver", + "cf-gears-authz-resolver", + "cf-gears-credstore", + "cf-gears-gear-orchestrator", + "cf-gears-grpc-hub", + "cf-gears-oidc-authn-plugin", + "cf-gears-resource-group", + "cf-gears-rg-tr-plugin", + "cf-gears-single-tenant-tr-plugin", + "cf-gears-static-authn-plugin", + "cf-gears-static-authz-plugin", + "cf-gears-static-credstore-plugin", + "cf-gears-static-idp-plugin", + "cf-gears-static-tr-plugin", + "cf-gears-tenant-resolver", + "cf-gears-toolkit", + "cf-gears-tr-authz-plugin", + "cf-gears-types-registry", + "clap", + "mimalloc", + "tokio", + "tracing", +] + [[package]] name = "cf-gears-resource-group" version = "0.3.2" @@ -3193,6 +3172,7 @@ dependencies = [ "serde", "serde_json", "thiserror 2.0.18", + "utoipa", "uuid", ] @@ -4093,6 +4073,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cluster-consumer" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "cf-gears-cluster-sdk", + "cf-gears-toolkit", + "cf-gears-toolkit-canonical-errors", + "cf-gears-toolkit-macros", + "clap", + "http", + "inventory", + "mimalloc", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "utoipa", +] + [[package]] name = "cmake" version = "0.1.58" @@ -5697,6 +5700,25 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hello" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "cf-gears-toolkit", + "cf-gears-toolkit-macros", + "clap", + "http", + "inventory", + "mimalloc", + "serde", + "tokio", + "tracing", + "utoipa", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -12053,8 +12075,10 @@ dependencies = [ "anyhow", "async-trait", "axum", + "cf-gears-authn-resolver", "cf-gears-authz-resolver", "cf-gears-authz-resolver-sdk", + "cf-gears-static-authn-plugin", "cf-gears-toolkit", "cf-gears-toolkit-canonical-errors", "cf-gears-toolkit-db", @@ -12064,6 +12088,8 @@ dependencies = [ "cf-gears-toolkit-odata", "cf-gears-toolkit-sdk", "cf-gears-toolkit-security", + "cf-gears-types-registry", + "clap", "futures-util", "http", "inventory", diff --git a/Cargo.toml b/Cargo.toml index 2ce91dd2b4..dc9d0280c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ debug = true [workspace] members = [ "apps/cf-gears-example-server", + "apps/platform-host", "tools/gts-analyze", "tools/xtask", "libs/toolkit", @@ -110,10 +111,8 @@ members = [ "examples/toolkit/users-info/users-info-sdk", "examples/toolkit/users-info/users-info", "examples/toolkit/users-info/users-info-server", - "examples/oop-gears/calculator-gateway/calculator-gateway-sdk", - "examples/oop-gears/calculator-gateway/calculator-gateway", - "examples/oop-gears/calculator/calculator-sdk", - "examples/oop-gears/calculator/calculator", + "examples/toolkit/hello/hello", + "examples/toolkit/cluster-consumer/cluster-consumer", "examples/cf-gears-fips-probe", "gears/system/types-registry/types-registry-sdk", "gears/system/types-registry/types-registry", diff --git a/Makefile b/Makefile index 3b245dd4a0..e29b003da8 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ COMMA := , EXAMPLE_SERVER_BIN ?= cf-gears-example-server EXAMPLE_SERVER_DEBUG_BINARY ?= target/debug/$(EXAMPLE_SERVER_BIN) EXAMPLE_SERVER_MANIFEST ?= apps/cf-gears-example-server/Cargo.toml -EXAMPLE_SERVER_FEATURE_EXCLUDES ?= default fips k8s otel oop-example timescaledb-usage-collector +EXAMPLE_SERVER_FEATURE_EXCLUDES ?= default fips k8s otel timescaledb-usage-collector EXAMPLE_SERVER_ALL_FEATURES := $(strip $(shell cargo gears ls features --manifest $(EXAMPLE_SERVER_MANIFEST) 2>/dev/null)) EXAMPLE_SERVER_FEATURES ?= $(subst $(SPACE),$(COMMA),$(filter-out $(EXAMPLE_SERVER_FEATURE_EXCLUDES),$(EXAMPLE_SERVER_ALL_FEATURES))) EXAMPLE_SERVER_FEATURE_ARGS ?= $(if $(EXAMPLE_SERVER_FEATURES),--features $(EXAMPLE_SERVER_FEATURES),) @@ -905,7 +905,7 @@ bench-db-longhaul: bench-pg-longhaul bench-mysql-longhaul bench-mariadb-longhaul # -------- E2E tests -------- -.PHONY: e2e e2e-local e2e-local-smoke e2e-mini-chat e2e-docker e2e-docker-smoke e2e-tr-authz e2e-usage-collector +.PHONY: e2e e2e-local e2e-local-smoke e2e-mini-chat e2e-docker e2e-docker-smoke e2e-tr-authz e2e-usage-collector e2e-oop oop-smoke E2E_TARGET ?= # E2E selectors for `make e2e-local`: @@ -984,6 +984,30 @@ e2e-usage-collector: $(call print_target_banner) $(MAKE) e2e-local SUITE=usage-collector +# Out-of-process (loopback) E2E: boots platform-host + OoP gears as local +# processes (no Kubernetes) and asserts the cross-process seams. Self-managed: +# its conftest builds the binaries and owns the process lifecycle, so it runs +# pytest directly (not via run_e2e.py). timeout_func_only keeps the per-test +# timeout from counting the heavy fixture setup (build + boot + route sync). +OOP_E2E_DIR ?= testing/e2e/suites/oop +OOP_E2E_TIMEOUT ?= 60 +## Run the out-of-process (loopback, no-k8s) E2E suite +e2e-oop: py-env + $(call print_target_banner) + cargo build -p cf-gears-platform-host --bin platform-host + cargo build -p hello --features oop_module --bin hello-oop + cargo build -p cf-api-contracts --features oop_module --bin api-contracts-oop + cargo build -p cf-api-contracts-consumer --features oop_module --bin api-contracts-consumer-oop + cargo build -p cf-gears-cluster --bin cluster-oop + cargo build -p cluster-consumer --features oop_module --bin cluster-consumer-oop + $(PYTHON) -m pytest $(OOP_E2E_DIR) -c testing/e2e/pytest.ini \ + -o timeout_func_only=true --timeout=$(OOP_E2E_TIMEOUT) -vv + +## Run the scripted Profile-3 (Kubernetes) OoP demo smoke test (see deploy/oop-smoke.sh --help) +oop-smoke: + $(call print_target_banner) + deploy/oop-smoke.sh $(OOP_SMOKE_ARGS) + # -------- Code coverage -------- .PHONY: coverage coverage-unit coverage-e2e-local check-prereq-e2e-local @@ -1233,12 +1257,6 @@ fips: $(call print_target_banner) cargo run --bin cf-gears-example-server --features fips,static-authn,static-authz,single-tenant,static-credstore,otel -- --config config/quickstart.yaml run -## Run server with out-of-process example gear -oop-example: - $(call print_target_banner) - cargo build -p calculator --features oop_gear - cargo run --bin cf-gears-example-server --features oop-example,users-info-example,static-authn,static-authz,static-tenants,static-credstore -- --config config/quickstart.yaml run - # Run all quality checks check: fmt cfs-validate clippy lychee security dylint gts-docs test $(call print_target_banner) diff --git a/apps/cf-gears-example-server/Cargo.toml b/apps/cf-gears-example-server/Cargo.toml index 39bc444865..8e9168d290 100644 --- a/apps/cf-gears-example-server/Cargo.toml +++ b/apps/cf-gears-example-server/Cargo.toml @@ -22,7 +22,6 @@ credstore = ["dep:credstore"] # FIPS-140-3: use the AWS-LC FIPS-validated crypto module for all cryptographic operations fips = ["toolkit/fips", "oagw", "api_egress?/fips"] users-info-example = ["dep:users-info"] -oop-example = ["dep:calculator-gateway", "dep:calculator"] single-tenant = ["dep:single-tenant-tr-plugin"] static-tenants = ["dep:static-tr-plugin"] static-authn = ["dep:static-authn-plugin"] @@ -119,8 +118,6 @@ bss_pricing = { package = "cf-gears-bss-pricing", path = "../../gears/bss/pricin # Optional example module users-info = { path = "../../examples/toolkit/users-info/users-info", optional = true } -calculator-gateway = { path = "../../examples/oop-gears/calculator-gateway/calculator-gateway", optional = true } -calculator = { path = "../../examples/oop-gears/calculator/calculator", optional = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/apps/cf-gears-example-server/src/registered_gears.rs b/apps/cf-gears-example-server/src/registered_gears.rs index 0385e77298..1dcec140c0 100644 --- a/apps/cf-gears-example-server/src/registered_gears.rs +++ b/apps/cf-gears-example-server/src/registered_gears.rs @@ -11,7 +11,7 @@ use authn_resolver as _; use authz_resolver as _; #[cfg(feature = "credstore")] use credstore as _; -#[cfg(all(feature = "file-parser", not(feature = "oop-example")))] +#[cfg(feature = "file-parser")] use file_parser as _; #[cfg(feature = "file-storage")] use file_storage as _; @@ -22,7 +22,7 @@ use grpc_hub as _; use nodes_registry as _; #[cfg(feature = "resource-group")] use resource_group as _; -#[cfg(all(feature = "simple-user-settings", not(feature = "oop-example")))] +#[cfg(feature = "simple-user-settings")] use simple_user_settings as _; use tenant_resolver as _; use types_registry as _; @@ -67,12 +67,6 @@ use chat_engine as _; #[cfg(feature = "users-info-example")] use users_info as _; -#[cfg(feature = "oop-example")] -use calculator_gateway as _; - -#[cfg(feature = "oop-example")] -use calculator as _; - #[cfg(feature = "static-idp")] use static_idp_plugin as _; diff --git a/apps/platform-host/Cargo.toml b/apps/platform-host/Cargo.toml new file mode 100644 index 0000000000..390eb382be --- /dev/null +++ b/apps/platform-host/Cargo.toml @@ -0,0 +1,112 @@ +[package] +name = "cf-gears-platform-host" +version = "0.1.0" +publish = false +edition.workspace = true +license.workspace = true +authors.workspace = true +description = "CF/Gears Platform Host - trust-coupled core + system gears (Profile 3 platform-host image)" + +[lints] +workspace = true + +[[bin]] +name = "platform-host" +path = "src/main.rs" + +[features] +# Plugin availability is a build-time choice (which plugin crates are linked); +# the active vendor is selected at runtime via GTS `vendor` config. Two presets +# are provided: +# * `dev-plugins` - static/config-driven plugins; zero external dependencies, +# boots out of the box (mirrors config/e2e-features.txt). +# Intended for CI, demos, and single-user on-prem. +# * `prod-plugins` - OIDC authn + tenant-resolver-backed authz + resource-group +# -backed tenant resolution. The real Profile 3 stack. +# Build the production platform-host image with `--features prod-plugins` +# (optionally `--no-default-features` to exclude the static plugins entirely). +default = ["dev-plugins"] + +dev-plugins = [ + "static-authn", + "static-authz", + "static-tenants", + "static-credstore", + "static-idp", +] + +# NOTE: credstore and account-management currently ship only `static-*` backend +# plugins in-tree, so they are included in both presets until real backends land. +prod-plugins = [ + "oidc-authn", + "tr-authz", + "tenant-resolver-rg", + "static-credstore", + "static-idp", +] + +# authn-resolver backends +static-authn = ["dep:static-authn-plugin"] +oidc-authn = ["dep:oidc-authn-plugin"] +# authz-resolver backends +static-authz = ["dep:static-authz-plugin"] +tr-authz = ["dep:tr-authz-plugin"] +# tenant-resolver backends +static-tenants = ["dep:static-tr-plugin"] +single-tenant = ["dep:single-tenant-tr-plugin"] +tenant-resolver-rg = ["dep:rg-tr-plugin"] +# credstore backends +static-credstore = ["dep:static-credstore-plugin"] +# account-management identity-provider backends +static-idp = ["dep:static-idp-plugin"] + +# FIPS-140-3: use the AWS-LC FIPS-validated crypto module for all crypto ops. +fips = ["toolkit/fips"] +# Kubernetes platform-plane auth (SA-token TokenReview validation). Enables the +# toolkit k8s-auth path AND the grpc-hub inbound TokenReview validator (the hub +# serves the DirectoryService whose RPCs are enforced on the platform plane). +k8s = ["toolkit/k8s-auth", "grpc_hub/k8s-auth"] +# OpenTelemetry tracing/metrics export. +otel = ["toolkit/otel"] + +[dependencies] +mimalloc = { version = "0.1" } + +# ToolKit bootstrap (host mode) +toolkit = { workspace = true, features = ["bootstrap"] } + +# Trust-coupled core (must remain co-located, see DESIGN "Platform Host Composition") +authz-resolver = { package = "cf-gears-authz-resolver", path = "../../gears/system/authz-resolver/authz-resolver" } +tenant-resolver = { package = "cf-gears-tenant-resolver", path = "../../gears/system/tenant-resolver/tenant-resolver" } +resource_group = { package = "cf-gears-resource-group", path = "../../gears/system/resource-group/resource-group" } +account_management = { package = "cf-gears-account-management", path = "../../gears/system/account-management/account-management" } + +# System gears +gear_orchestrator = { package = "cf-gears-gear-orchestrator", path = "../../gears/system/gear-orchestrator" } +types_registry = { package = "cf-gears-types-registry", path = "../../gears/system/types-registry/types-registry" } +credstore = { package = "cf-gears-credstore", path = "../../gears/credstore/credstore" } +api_gateway = { package = "cf-gears-api-gateway", path = "../../gears/system/api-gateway" } +grpc_hub = { package = "cf-gears-grpc-hub", path = "../../gears/system/grpc-hub" } +authn-resolver = { package = "cf-gears-authn-resolver", path = "../../gears/system/authn-resolver/authn-resolver" } + +# Optional tenant resolver plugins +static-tr-plugin = { package = "cf-gears-static-tr-plugin", path = "../../gears/system/tenant-resolver/plugins/static-tr-plugin", optional = true } +single-tenant-tr-plugin = { package = "cf-gears-single-tenant-tr-plugin", path = "../../gears/system/tenant-resolver/plugins/single-tenant-tr-plugin", optional = true } +rg-tr-plugin = { package = "cf-gears-rg-tr-plugin", path = "../../gears/system/tenant-resolver/plugins/rg-tr-plugin", optional = true } + +# Optional authn/authz plugins +static-authn-plugin = { package = "cf-gears-static-authn-plugin", path = "../../gears/system/authn-resolver/plugins/static-authn-plugin", optional = true } +oidc-authn-plugin = { package = "cf-gears-oidc-authn-plugin", path = "../../gears/system/authn-resolver/plugins/oidc-authn-plugin", optional = true } +static-authz-plugin = { package = "cf-gears-static-authz-plugin", path = "../../gears/system/authz-resolver/plugins/static-authz-plugin", optional = true } +tr-authz-plugin = { package = "cf-gears-tr-authz-plugin", path = "../../gears/system/authz-resolver/plugins/tr-authz-plugin", optional = true } + +# Optional credstore plugins +static-credstore-plugin = { package = "cf-gears-static-credstore-plugin", path = "../../gears/credstore/plugins/static-credstore-plugin", optional = true } + +# Optional account-management identity-provider plugins +static-idp-plugin = { package = "cf-gears-static-idp-plugin", path = "../../gears/system/account-management/plugins/static-idp-plugin", optional = true } + +anyhow = { workspace = true } +tokio = { workspace = true } +clap = { workspace = true } +tracing = { workspace = true } diff --git a/apps/platform-host/src/main.rs b/apps/platform-host/src/main.rs new file mode 100644 index 0000000000..c4de8fd9ba --- /dev/null +++ b/apps/platform-host/src/main.rs @@ -0,0 +1,71 @@ +mod registered_gears; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use mimalloc::MiMalloc; +use std::path::PathBuf; +use toolkit::bootstrap::{AppConfig, list_gear_names, run_migrate, run_server}; + +#[global_allocator] +static GLOBAL: MiMalloc = MiMalloc; + +/// CF/Gears Platform Host - trust-coupled core + system gears. +#[derive(Parser)] +#[command(name = "platform-host")] +#[command(about = "CF/Gears Platform Host - trust-coupled core + system gears")] +#[command(version = env!("CARGO_PKG_VERSION"))] +struct Cli { + /// Path to configuration file + #[arg(short, long)] + config: Option, + + /// Print effective configuration (YAML) and exit + #[arg(long)] + print_config: bool, + + /// List all configured gear names and exit + #[arg(long)] + list_gears: bool, + + /// Log verbosity level (-v debug, -vv trace) + #[arg(short, long, action = clap::ArgAction::Count)] + verbose: u8, + + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + /// Start the server + Run, + /// Run database migrations and exit (for cloud deployments) + Migrate, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + let mut config = AppConfig::load_or_default(cli.config.as_ref())?; + config.apply_cli_overrides(cli.verbose); + + if cli.print_config { + println!("Effective configuration:\n{}", config.to_yaml()?); + return Ok(()); + } + + if cli.list_gears { + let gears = list_gear_names(&config); + println!("Configured gears ({}):", gears.len()); + for gear in gears { + println!(" - {gear}"); + } + return Ok(()); + } + + match cli.command.unwrap_or(Commands::Run) { + Commands::Run => run_server(config).await, + Commands::Migrate => run_migrate(config).await, + } +} diff --git a/apps/platform-host/src/registered_gears.rs b/apps/platform-host/src/registered_gears.rs new file mode 100644 index 0000000000..0756d21648 --- /dev/null +++ b/apps/platform-host/src/registered_gears.rs @@ -0,0 +1,50 @@ +// Ensure all platform-host gears are linked and registered via inventory. +// +// Unlike `cf-gears-example-server`, this crate links ONLY the trust-coupled +// core + system gears that make up the platform-host image (see DESIGN +// "Platform Host Composition"). Gear isolation for OoP images is achieved by +// the dependency graph, not `#[cfg]` gates. +#![allow(unused_imports)] + +// Trust-coupled core +use account_management as _; +use authz_resolver as _; +use resource_group as _; +use tenant_resolver as _; + +// System gears +use api_gateway as _; +use authn_resolver as _; +use credstore as _; +use gear_orchestrator as _; +use grpc_hub as _; +use types_registry as _; + +// === Plugins (selected via Cargo features; active vendor chosen by config) === + +#[cfg(feature = "static-authn")] +use static_authn_plugin as _; + +#[cfg(feature = "oidc-authn")] +use oidc_authn_plugin as _; + +#[cfg(feature = "static-authz")] +use static_authz_plugin as _; + +#[cfg(feature = "tr-authz")] +use tr_authz_plugin as _; + +#[cfg(feature = "static-tenants")] +use static_tr_plugin as _; + +#[cfg(feature = "single-tenant")] +use single_tenant_tr_plugin as _; + +#[cfg(feature = "tenant-resolver-rg")] +use rg_tr_plugin as _; + +#[cfg(feature = "static-credstore")] +use static_credstore_plugin as _; + +#[cfg(feature = "static-idp")] +use static_idp_plugin as _; diff --git a/config/oop-api-contracts-consumer.yaml b/config/oop-api-contracts-consumer.yaml new file mode 100644 index 0000000000..2aafbe2b61 --- /dev/null +++ b/config/oop-api-contracts-consumer.yaml @@ -0,0 +1,48 @@ +# Local multi-process platform-host: api-contracts-consumer OoP gear. +# +# Run (after the host + the api-contracts provider are up): +# TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ +# cargo run -p cf-api-contracts-consumer --features oop_module --bin api-contracts-consumer-oop \ +# -- --config config/oop-api-contracts-consumer.yaml +# +# Then (bearer required — any non-empty token is accepted by static-authn +# accept_all and mapped to the platform-root tenant): +# curl -X POST -H 'Authorization: Bearer test-token' -H 'Content-Type: application/json' \ +# -d '{"amount_cents":1000,"currency":"USD","description":"test"}' \ +# http://127.0.0.1:9098/api-contracts-consumer/v1/charge +# +# The consumer resolves the PaymentApi contract from the SEPARATE api-contracts +# provider pod over REST (via the DirectoryService) and forwards the charge — +# the OoP gear-to-gear REST path. + +server: + home_dir: "~/.cf-gears-api-contracts-consumer" + +logging: + default: + console_level: info + file: "logs/oop-api-contracts-consumer.log" + file_level: info + +oop_http: + listen_addr: "127.0.0.1:9098" + advertise_uri: "http://127.0.0.1:9098" + allow_loopback_advertise: true + +gears: + api-contracts-consumer: + config: {} + + # Embedded tenant-plane authn stack (in-memory; no DB). + authn-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + types-registry: + config: {} diff --git a/config/oop-api-contracts.yaml b/config/oop-api-contracts.yaml new file mode 100644 index 0000000000..a7eda5a513 --- /dev/null +++ b/config/oop-api-contracts.yaml @@ -0,0 +1,41 @@ +# Local multi-process platform-host: api-contracts PaymentApi PROVIDER OoP gear. +# +# Run (after the host is up): +# TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ +# cargo run -p cf-api-contracts --features oop_module --bin api-contracts-oop -- --config config/oop-api-contracts.yaml +# +# The pod authenticates the bearer locally (embedded authn stack) and serves the +# PaymentApi REST contract at /api-contracts/v1/... . Consumer pods discover this +# endpoint via the DirectoryService and call it over REST (OoP gear-to-gear). + +server: + home_dir: "~/.cf-gears-api-contracts" + +logging: + default: + console_level: info + file: "logs/oop-api-contracts.log" + file_level: info + +oop_http: + listen_addr: "127.0.0.1:9097" + advertise_uri: "http://127.0.0.1:9097" + allow_loopback_advertise: true + +gears: + api-contracts: + config: {} + + # Embedded tenant-plane authn stack (in-memory; no DB). + authn-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + types-registry: + config: {} diff --git a/config/oop-cluster-consumer.yaml b/config/oop-cluster-consumer.yaml new file mode 100644 index 0000000000..7c25861250 --- /dev/null +++ b/config/oop-cluster-consumer.yaml @@ -0,0 +1,37 @@ +# Local multi-process platform-host: cluster-consumer OoP gear. +# +# Run (after the host + cluster gear are up): +# POD_NAMESPACE=platform-test \ +# TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ +# cargo run -p cluster-consumer --features oop_module --bin cluster-consumer-oop \ +# -- --config config/oop-cluster-consumer.yaml +# +# Then (reserve a seat, then read the venue inventory): +# curl -X POST http://127.0.0.1:8087/cluster-consumer/v1/reservations \ +# -H 'Content-Type: application/json' -d '{"seat":"A12","holder":"me"}' +# curl http://127.0.0.1:8087/cluster-consumer/v1/inventory +# +# POD_NAMESPACE is what cluster_sdk::wiring uses to derive the cluster gRPC +# endpoint (cluster.{POD_NAMESPACE}.svc.cluster.local:50051). On loopback that +# name does not resolve, so a coordination call returns a typed ConnectionLost +# (503) — the seam proof. In Kubernetes the downward API supplies POD_NAMESPACE +# and the DNS name resolves, so the reservation flow succeeds. The cluster-free +# /ping and /status routes answer either way. + +server: + home_dir: "~/.cf-gears-cluster-consumer" + +logging: + default: + console_level: info + file: "logs/oop-cluster-consumer.log" + file_level: info + +oop_http: + listen_addr: "127.0.0.1:9093" + advertise_uri: "http://127.0.0.1:9093" + allow_loopback_advertise: true + +gears: + cluster-consumer: + config: {} diff --git a/config/oop-cluster.yaml b/config/oop-cluster.yaml new file mode 100644 index 0000000000..ba740cc745 --- /dev/null +++ b/config/oop-cluster.yaml @@ -0,0 +1,46 @@ +# Local multi-process platform-host: cluster OoP gear (standalone backend). +# +# Run (after the host is up): +# TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ +# cargo run -p cf-gears-cluster --bin cluster-oop -- --config config/oop-cluster.yaml +# +# Serves its coordination gRPC plane on 127.0.0.1:50061 (its own grpc-hub) and +# the OoP HTTP probes on 127.0.0.1:9092. It registers presence with the +# platform-host DirectoryService (TOOLKIT_DIRECTORY_ENDPOINT), NOT on :50051. +# +# NOTE (loopback): a cluster *consumer* derives the cluster endpoint from +# Kubernetes DNS convention (cluster.{POD_NAMESPACE}.svc.cluster.local:50051, +# see cluster_sdk::wiring) — there is no client-side endpoint key by design +# (invariant I9). On plain loopback that name does not resolve, so a consumer's +# round-trip returns a typed ConnectionLost. The successful round-trip is the +# Kubernetes demo's job. This gear still boots, serves its probes, and registers. + +server: + home_dir: "~/.cf-gears-cluster" + +logging: + default: + console_level: info + file: "logs/oop-cluster.log" + file_level: info + +# OoP HTTP serving lifecycle: bind the local probe/REST server + register with +# the DirectoryService so the edge and consumers can discover this instance. +oop_http: + listen_addr: "127.0.0.1:9092" + advertise_uri: "http://127.0.0.1:9092" + allow_loopback_advertise: true + +gears: + # cluster's own gRPC hub hosts its coordination plane. Required — without a + # configured, linked hub the registry fails with GrpcRequiresHub. + grpc-hub: + config: + listen_addr: "127.0.0.1:50061" + # One standalone profile named `demo`, matched by the consumer's DemoProfile + # marker (NAME = "demo"). standalone is in-memory: no infrastructure needed. + cluster: + config: + profiles: + demo: + cache: { provider: standalone } diff --git a/config/oop-example-follower.yaml b/config/oop-example-follower.yaml deleted file mode 100644 index ddf01b1daa..0000000000 --- a/config/oop-example-follower.yaml +++ /dev/null @@ -1,19 +0,0 @@ -server: - home_dir: "~/.cf-gears" - -# OoP gears receive this config via --config CLI arg or MODULE_CONFIG_PATH env var from master host. -# Directory endpoint is read from the shared config file. - -gears: - grpc-hub: - config: - # Ephemeral, registers itself in Directory - listen_addr: "127.0.0.1:0" - - calculator: - # Note: In OoP config, runtime.execution is not needed - it's configured in master config. - config: { } - -logging: - default: - console_level: debug \ No newline at end of file diff --git a/config/oop-example-master+follower.yaml b/config/oop-example-master+follower.yaml deleted file mode 100644 index caad6cb7d3..0000000000 --- a/config/oop-example-master+follower.yaml +++ /dev/null @@ -1,31 +0,0 @@ -server: - home_dir: "~/.cf-gears" - -gears: - api-gateway: - config: - bind_addr: "127.0.0.1:8087" - enable_docs: true - cors_enabled: false - auth_disabled: true - - grpc-hub: - config: - listen_addr: "127.0.0.1:0" # ephemeral port, passed to OoP child via env - - calculator-gateway: - config: { } - - gear-orchestrator: - config: { } - - calculator: - runtime: - type: oop - execution: - executable_path: "./target/debug/calculator-oop" - args: [ "--config", "config/oop-example-follower.yaml" ] - -logging: - default: - console_level: debug diff --git a/config/oop-example-master.yaml b/config/oop-example-master.yaml deleted file mode 100644 index 50b06ccc1c..0000000000 --- a/config/oop-example-master.yaml +++ /dev/null @@ -1,28 +0,0 @@ -server: - home_dir: "~/.cf-gears" - -# OoP gears receive this config via --config CLI arg or MODULE_CONFIG_PATH env var from master host. -# Directory endpoint is read from the shared config file. - -gears: - api-gateway: - config: - bind_addr: "127.0.0.1:8087" - enable_docs: true - cors_enabled: false - auth_disabled: true - - grpc-hub: - config: - # fixed port so the child can find it - listen_addr: "127.0.0.1:50051" - - gear-orchestrator: - config: { } - - calculator-gateway: - config: { } - -logging: - default: - console_level: debug diff --git a/config/oop-hello.yaml b/config/oop-hello.yaml new file mode 100644 index 0000000000..62bb53bee8 --- /dev/null +++ b/config/oop-hello.yaml @@ -0,0 +1,29 @@ +# Local multi-process platform-host: hello OoP gear. +# +# Run (after the host is up): +# TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ +# cargo run -p hello --features oop_module --bin hello-oop -- --config config/oop-hello.yaml +# +# Then: +# curl http://127.0.0.1:8087/hello/v1/ping # proxied through the edge +# curl http://127.0.0.1:9091/hello/v1/ping # direct to the OoP pod + +server: + home_dir: "~/.cf-gears-hello" + +logging: + default: + console_level: info + file: "logs/oop-hello.log" + file_level: info + +# OoP HTTP serving lifecycle: bind a local REST server + register its endpoint +# with the DirectoryService so the edge can proxy to it. +oop_http: + listen_addr: "127.0.0.1:9091" + advertise_uri: "http://127.0.0.1:9091" + allow_loopback_advertise: true + +gears: + hello: + config: {} diff --git a/config/oop-host.yaml b/config/oop-host.yaml new file mode 100644 index 0000000000..3c7ec3a871 --- /dev/null +++ b/config/oop-host.yaml @@ -0,0 +1,135 @@ +# Local multi-process platform-host: platform-host (edge + DirectoryService). +# +# Run: +# cargo run -p cf-gears-platform-host -- --config config/oop-host.yaml run +# +# grpc-hub listens on TCP so a separate OoP gear process can register; the +# api-gateway edge reverse-proxies discovered OoP routes. + +server: + home_dir: "~/.cf-gears-host" + +database: + servers: + sqlite_users: + engine: "sqlite" + params: + WAL: "true" + synchronous: "NORMAL" + busy_timeout: "5000" + pool: + max_conns: 5 + acquire_timeout: "30s" + +logging: + default: + console_level: info + file: "logs/oop-host.log" + file_level: info + +gears: + api-gateway: + config: + bind_addr: "127.0.0.1:8087" + enable_docs: true + cors_enabled: false + openapi: + title: "CF/Gears Host API" + version: "0.1.0" + description: "CF/Gears platform host" + defaults: + body_limit_bytes: 64000000 + rate_limit: + rps: 1000 + burst: 200 + in_flight: 64 + auth_disabled: false + require_auth_by_default: true + gateway_proxy: + enabled: true + directory_endpoint: "http://127.0.0.1:50051" + sync_interval_secs: 5 + + gear-orchestrator: + config: {} + + grpc-hub: + config: + listen_addr: "127.0.0.1:50051" + + authn-resolver: + config: + vendor: "constructorfabric" + + authz-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + static-authz-plugin: + config: + vendor: "constructorfabric" + priority: 100 + + tenant-resolver: + config: + vendor: "constructorfabric" + + static-tr-plugin: + config: + vendor: "constructorfabric" + priority: 20 + tenants: + - id: "00000000-df51-5b42-9538-d2b56b7ee953" + name: "platform-root" + parent_id: null + status: active + + types-registry: + config: + entities: + - $id: "gts://gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + $schema: "http://json-schema.org/draft-07/schema#" + description: "Platform-root tenant type (no parents)." + type: "object" + x-gts-traits: + allowed_parent_types: [] + idp_provisioning: false + + account-management: + database: + server: "sqlite_users" + file: "account_management.db" + config: + bootstrap: + root_id: "00000000-df51-5b42-9538-d2b56b7ee953" + root_name: "platform-root" + root_tenant_type: "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + idp_wait_timeout: "5m" + idp_retry_backoff_initial: "2s" + idp_retry_backoff_max: "30s" + strict: false + idp: + required: true + + resource-group: + database: + server: "sqlite_users" + file: "resource_group.db" + config: {} + + credstore: + database: + server: "sqlite_users" + file: "credstore.db" + config: + vendor: "constructorfabric" + + static-credstore-plugin: + config: + secrets: [] diff --git a/config/oop-users-info.yaml b/config/oop-users-info.yaml new file mode 100644 index 0000000000..57c938a937 --- /dev/null +++ b/config/oop-users-info.yaml @@ -0,0 +1,68 @@ +# Local multi-process platform-host: users-info OoP gear (authenticated + DB). +# +# Run (after the host is up): +# TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ +# cargo run -p users-info-oop --bin users-info-oop -- --config config/oop-users-info.yaml +# +# Then (bearer required — any non-empty token is accepted by static-authn +# accept_all and mapped to the platform-root tenant): +# curl -H 'Authorization: Bearer test-token' http://127.0.0.1:8087/users-info/v1/users # via edge +# curl -H 'Authorization: Bearer test-token' http://127.0.0.1:9092/users-info/v1/users # direct +# +# The pod authenticates the bearer locally (embedded authn stack), then calls +# the platform-host's authz-resolver over REST (resolved via DirectoryService) +# for every PEP check, and serves from its own SQLite database. + +server: + home_dir: "~/.cf-gears-users-info" + +database: + servers: + users_db: + engine: "sqlite" + params: + WAL: "true" + synchronous: "NORMAL" + busy_timeout: "5000" + pool: + max_conns: 5 + acquire_timeout: "30s" + +logging: + default: + console_level: info + file: "logs/oop-users-info.log" + file_level: info + +# OoP HTTP serving lifecycle: bind a local REST server + register its endpoint +# with the DirectoryService so the edge can proxy to it. +oop_http: + listen_addr: "127.0.0.1:9092" + advertise_uri: "http://127.0.0.1:9092" + allow_loopback_advertise: true + +gears: + # Target gear: needs the db capability + its own config. + users-info: + database: + server: "users_db" + file: "users_info.db" + config: + default_page_size: 50 + max_page_size: 1000 + audit_base_url: "http://audit.local" + notifications_base_url: "http://notifications.local" + + # Embedded tenant-plane authn stack (in-memory; no DB). + authn-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + types-registry: + config: {} diff --git a/config/platform-host.yaml b/config/platform-host.yaml new file mode 100644 index 0000000000..e3c8b925a5 --- /dev/null +++ b/config/platform-host.yaml @@ -0,0 +1,160 @@ +# CF/Gears Platform Host configuration (dev-plugins preset). +# +# Tailored to the `apps/platform-host` composition: trust-coupled core +# (authz-resolver, tenant-resolver, resource-group, account-management) + system +# gears (gear-orchestrator, types-registry, credstore, api-gateway, grpc-hub) + +# embedded authn-resolver, wired with the static/dev plugin set so it boots with +# zero external dependencies (SQLite + config-driven plugins). +# +# For the production image build (`--no-default-features --features prod-plugins`) +# supply a config selecting the OIDC/tr-authz/rg-tr vendors and a real database. + +server: + home_dir: "~/.cf-gears-platform-host" + +database: + servers: + sqlite_users: + engine: "sqlite" + params: + WAL: "true" + synchronous: "NORMAL" + busy_timeout: "5000" + pool: + max_conns: 5 + acquire_timeout: "30s" + +logging: + default: + console_level: info + file: "logs/platform-host.log" + file_level: info + max_age_days: 28 + max_backups: 3 + max_size_mb: 10 + +gears: + api-gateway: + config: + # Bind all interfaces so the gear is reachable inside a container. + bind_addr: "0.0.0.0:8087" + enable_docs: true + cors_enabled: false + openapi: + title: "CF/Gears Platform Host API" + version: "0.1.0" + description: "CF/Gears Platform Host API Documentation" + defaults: + body_limit_bytes: 64000000 + rate_limit: + rps: 1000 + burst: 200 + in_flight: 64 + auth_disabled: false + require_auth_by_default: true + + gear-orchestrator: + config: {} + + grpc-hub: + config: + listen_addr: "uds:///tmp/cf-gears-platform-host-grpc" + + # --- AuthN / AuthZ pipeline (static/dev plugins) --- + authn-resolver: + config: + vendor: "constructorfabric" + + authz-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + static-authz-plugin: + config: + vendor: "constructorfabric" + priority: 100 + + # --- Tenant resolution (static/dev plugin) --- + tenant-resolver: + config: + vendor: "constructorfabric" + + static-tr-plugin: + config: + vendor: "constructorfabric" + priority: 20 + tenants: + # Structural root; must match DEFAULT_TENANT_ID in toolkit-security + # (used by auth-disabled mode and as the account-management root anchor). + - id: "00000000-df51-5b42-9538-d2b56b7ee953" + name: "platform-root" + parent_id: null + status: active + + # --- Types Registry --- + # The AM envelope type schemas (`cf.core.am.tenant_type.v1~` and + # `cf.core.am.tenant_metadata.v1~`) are auto-seeded at link time via the AM + # SDK's `inventory::submit!` declarations. The concrete platform-root tenant + # type is operator-controlled and seeded here so account-management's + # bootstrap preflight can resolve `root_tenant_type`. + types-registry: + config: + entities: + - $id: "gts://gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + $schema: "http://json-schema.org/draft-07/schema#" + description: "Platform-root tenant type (no parents)." + type: "object" + x-gts-traits: + allowed_parent_types: [] + idp_provisioning: false + - $id: "gts://gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~" + $schema: "http://json-schema.org/draft-07/schema#" + description: "Customer tenant under the platform root." + type: "object" + x-gts-traits: + allowed_parent_types: + - "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + - "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~" + idp_provisioning: false + + # --- Account Management --- + account-management: + database: + server: "sqlite_users" + file: "account_management.db" + config: + bootstrap: + root_id: "00000000-df51-5b42-9538-d2b56b7ee953" + root_name: "platform-root" + root_tenant_type: "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + idp_wait_timeout: "5m" + idp_retry_backoff_initial: "2s" + idp_retry_backoff_max: "30s" + strict: false + idp: + required: true + + # --- Resource Group --- + resource-group: + database: + server: "sqlite_users" + file: "resource_group.db" + config: {} + + # --- Credstore --- + credstore: + database: + server: "sqlite_users" + file: "credstore.db" + config: + vendor: "constructorfabric" + + static-credstore-plugin: + config: + secrets: [] diff --git a/config/quickstart-windows.yaml b/config/quickstart-windows.yaml deleted file mode 100644 index e53667a7c0..0000000000 --- a/config/quickstart-windows.yaml +++ /dev/null @@ -1,349 +0,0 @@ -# CF/Gears Example Server Configuration -# -# Database Architecture Overview: -# =============================== -# -# CF/Gears Example Server uses a centralized DbManager that: -# 1. Loads global database server templates from `database.servers` -# 2. For each gear, merges server template with gear-specific overrides -# 3. Creates isolated database connections per gear -# 4. Caches DbHandle instances for reuse within the same gear -# -# Key principles: -# - Each gear gets its own database connection (no sharing) -# - SQLite gears automatically get their own database file -# - Configuration follows a clear precedence hierarchy -# - Environment variables are expanded (${VAR_NAME}) -# - All paths are resolved relative to server.home_dir - -# Core server configuration (global section) -server: - home_dir: "~/.cf-gears" - -# Database configuration (DbManager architecture) -# -# The database config has two levels: -# 1. Global "servers" - shared connection templates that gears can reference -# 2. Per-gear configs - specific database settings for each gear -# -# Configuration precedence (highest to lowest): -# - Gear `params` map (highest priority) -# - Gear DSN query params -# - Gear fields (host, port, user, etc.) -# - Gear DSN base -# - Server `params` map -# - Server DSN query params -# - Server fields -# - Server DSN base (lowest priority) -# -database: - # Global database servers - reusable connection templates - # These define shared settings that multiple gears can inherit - servers: - # SQLite server template for user data gears - # Note: No DSN or file specified here - gears specify their own database files - # This prevents accidental sharing of SQLite databases between gears - sqlite_users: - engine: "sqlite" - # SQLite PRAGMA parameters applied to all connections using this server - params: - WAL: "true" # Enable Write-Ahead Logging for better concurrency - synchronous: "NORMAL" # Balance between safety and performance - busy_timeout: "5000" # Wait 5 seconds for locked database - # Connection pool settings shared by all gears using this server - pool: - max_conns: 5 # Maximum 5 concurrent connections - acquire_timeout: "30s" # Timeout when acquiring connection from pool - - # Global database options - # auto_provision: true # (default) Create directories automatically for SQLite files - # auto_provision: false # Fail if directories don't exist - -# Logging configuration (global section) -logging: - # global section - default: - console_level: info - file: "logs/cf-gears.log" - file_level: info - max_age_days: 28 - max_backups: 3 - max_size_mb: 10 - sqlx: - console_level: debug - file: "logs/sql.log" - file_level: debug - api-gateway: - console_level: info - file: "logs/api.log" - file_level: debug - max_age_days: 28 - max_backups: 3 - max_size_mb: 100 - calculator: - console_level: debug - file: "logs/calculator-oop.log" - file_level: debug - max_age_days: 28 - max_backups: 3 - max_size_mb: 100 - - -# Per-gear configurations using new structure -gears: - api-gateway: - config: - bind_addr: "0.0.0.0:8087" - enable_docs: true - cors_enabled: false - - # OpenAPI Documentation Configuration - openapi: - title: "CF/Gears Example Server API" - version: "0.1.0" - description: "CF/Gears Example Server API Documentation" - - # Authentication Configuration - auth_disabled: true - require_auth_by_default: true - - gear-orchestrator: - config: {} - # No configuration needed for the gear orchestrator - - grpc-hub: - config: - # Use ephemeral port; OoP gears receive the actual bound address from master host - listen_addr: "127.0.0.1:0" - - calculator: - runtime: - type: oop - execution: - executable_path: "calculator-oop.exe" - # CLI arguments passed to OoP gear. Use --config to pass gear's own config file: - # args: ["--config", "config/oop-example.yaml"] - # The local config file is used to: - # - Configure system gears for OoP process (e.g., grpc_hub.listen_addr) - # - Override settings from master (TOOLKIT_MODULE_CONFIG) - # - Run in standalone mode (without master host) - args: [ "--config", "./config/oop-example.yaml" ] - working_directory: "." - environment: { } - config: - some_setting: "value" - - - users-info: - # Gear-specific database configuration - database: - # Reference to global server template defined above - server: "sqlite_users" # Inherits params and pool settings from sqlite_users - - # Gear-specific database file (required for SQLite gears) - file: "users_info.db" # Creates: ~/.cf-gears/users_info/users_info.db - - # Gear can override server settings: - # params: - # synchronous: "FULL" # Would override server's "NORMAL" setting - # custom_pragma: "value" # Would add new PRAGMA setting - # pool: - # max_conns: 10 # Would override server's max_conns: 5 - - # Alternative ways to specify database location: - # path: "/absolute/path/to/db.sqlite" # Use absolute path instead of file - # dsn: "sqlite://custom.db?WAL=false" # Complete DSN override (ignores server) - - # For other database types (PostgreSQL, MySQL): - # host: "localhost" # Override server host - # port: 5432 # Override server port - # user: "app_user" # Override server user - # password: "${DB_PASSWORD}" # Environment variable expansion - # dbname: "users_db" # Override server database name - - # Gear-specific application configuration - config: - default_page_size: 5 - max_page_size: 100 - - tenant-resolver: - config: - vendor: "constructorfabric" - static-tr-plugin: - config: - vendor: "constructorfabric" - priority: 20 - # Tenant tree configuration (example: company with 2 divisions, each with departments) - tenants: - # Root tenant (company level) - - id: "00000000-0000-0000-0000-000000000001" - name: "company-root" - parent_id: null - status: active - - # Division 1: Engineering - - id: "00000000-0000-0000-0000-000000000010" - name: "engineering" - parent_id: "00000000-0000-0000-0000-000000000001" - status: active - - # Division 2: Sales - - id: "00000000-0000-0000-0000-000000000011" - name: "sales" - parent_id: "00000000-0000-0000-0000-000000000001" - status: active - - # Department under Engineering: Backend - - id: "00000000-0000-0000-0000-000000000100" - name: "backend" - parent_id: "00000000-0000-0000-0000-000000000010" - status: active - - # Department under Engineering: Frontend (soft-deleted) - - id: "00000000-0000-0000-0000-000000000101" - name: "frontend" - parent_id: "00000000-0000-0000-0000-000000000010" - status: deleted - - # Department under Sales: Enterprise - - id: "00000000-0000-0000-0000-000000000110" - name: "enterprise" - parent_id: "00000000-0000-0000-0000-000000000011" - status: active - - credstore: - database: - server: "sqlite_users" - file: "credstore.db" - config: - # Gateway selects its value-store backend plugin by GTS vendor; - # must match static-credstore-plugin (default "constructorfabric"). - vendor: "constructorfabric" - - static-credstore-plugin: - config: - secrets: - - tenant_id: "00000000-df51-5b42-9538-d2b56b7ee953" - owner_id: "00000000-df51-5b42-9538-d2b56b7ee953" - key: "openai-key" - value: "sk-test-e2e-fake-key" - - # File parser gear configuration (no database needed) - file-parser: - config: - allowed_local_base_dir: "C:\\Temp" - -# OpenTelemetry configuration (resource identity, tracing, metrics) -opentelemetry: - # Shared resource identity (attached to all traces and metrics) - resource: - service_name: "cf-gears-api" - attributes: - service.version: "1.3.7" - deployment.environment: "dev" - service.namespace: "cf-gears" - - # Default exporter shared by tracing and metrics. - # Per-signal `exporter` fields override this when present. - exporter: - kind: "otlp_grpc" # or "otlp_http" - endpoint: "http://127.0.0.1:4317" - headers: - # Add any required headers, e.g., for authentication - # authorization: "Bearer your-token" - uptrace-dsn: "http://project1_secret@localhost:14318?grpc=14317" - timeout_ms: 5000 - - # Tracing configuration - tracing: - enabled: false - - # Per-signal exporter override (fully replaces the shared exporter) - exporter: - kind: "otlp_grpc" - endpoint: "http://127.0.0.1:14317" # Jaeger OTLP gRPC endpoint - - # Sampling configuration - sampler: - parent_based_ratio: # parent_based_always_on | parent_based_ratio | always_on | always_off - ratio: 1.0 # Sample 100% of traces - - # Propagation configuration - propagation: - w3c_trace_context: true # Enable W3C Trace Context propagation - - # HTTP-specific options - http: - inject_request_id_header: "x-request-id" - # WARNING: "authorization" sends bearer tokens to the trace collector — dev/test only - record_headers: [ "user-agent", "x-forwarded-for", "authorization" ] - - # Log correlation (future feature) - logs_correlation: - inject_trace_ids_into_logs: true - - # Metrics configuration - metrics: - enabled: false - -# Example configurations for different database scenarios: -# -# Example 1: PostgreSQL server with multiple gears -# ================================================== -# database: -# servers: -# main_postgres: -# host: "localhost" -# port: 5432 -# user: "cf_gears" -# password: "${POSTGRES_PASSWORD}" # From environment variable -# params: -# sslmode: "require" -# application_name: "cf_gears" -# pool: -# max_conns: 20 -# acquire_timeout: "30s" -# gears: -# users_gear: -# database: -# server: "main_postgres" -# dbname: "users_db" # Each gear gets its own database -# orders_gear: -# database: -# server: "main_postgres" -# dbname: "orders_db" # Separate database for orders -# pool: -# max_conns: 10 # Gear-specific pool override -# -# Example 2: Mixed database types -# =============================== -# database: -# servers: -# postgres_main: -# dsn: "postgres://user:${PG_PASS}@localhost:5432/main" -# redis_cache: -# dsn: "redis://localhost:6379/0" -# gears: -# user_service: -# database: -# server: "postgres_main" -# dbname: "users" -# cache_service: -# database: -# server: "redis_cache" -# -# Example 3: Complete DSN override -# ================================ -# gears: -# analytics: -# database: -# dsn: "postgres://analytics:${ANALYTICS_PASSWORD}@analytics-db:5432/metrics?sslmode=require" -# # When DSN is specified, server reference is ignored -# -# Example 4: No database gear -# ============================= -# gears: -# static_files: -# # No database section - gear won't get database access -# config: -# static_dir: "/var/www/static" diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000000..3d97756d31 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,504 @@ +# CF/Gears Profile 3 (Kubernetes) Deployment + +A thin, end-to-end deployment guide for the out-of-process (OoP) gear +architecture. Every gear runs as its **own pod**; all external traffic enters +through the **platform-host** edge, which discovers pods via the DirectoryService +and reverse-proxies to them. + +### Pods + +- **platform-host** — one pod running the trust-coupled core (authz-resolver, + tenant-resolver, resource-group, account-management) + system gears + (gear-orchestrator, types-registry, credstore, **api-gateway** edge, + **grpc-hub** DirectoryService) + embedded authn-resolver. +- **hello** — **anonymous** REST gear (no auth, no DB): the minimal cross-pod + reverse-proxy case. +- **users-info** — **authenticated + remote-authz + Postgres**: the full OoP + path (authenticate the bearer locally → central authz-resolver over REST → own DB). +- **api-contracts** / **api-contracts-consumer** — an **OoP→OoP pair**: the + consumer pod calls the provider pod over REST (`PaymentApi`), discovered via + the DirectoryService (the consumer binary does not link the provider). +- **shared-postgres** — one PostgreSQL pod serving a **database per gear** + (`postgres` chart); gears never share tables — cross-gear reads go through SDK + contracts, not SQL. + +### Interactions verified + +| # | Interaction | Exercised by | Verified via | +|---|-------------|--------------|--------------| +| 1 | Anonymous edge → pod reverse-proxy | `hello` ping | `served_by` = the hello pod's pid | +| 2 | Authenticated edge → pod (bearer forwarded) | `users-info`, `api-contracts-consumer` | `200`/`201` with `Authorization: Bearer` | +| 3 | Tenant-plane auth: **missing token → 401** | every `.authenticated()` route | `%{http_code}` = `401` (no-token curls) | +| 4 | PEP → PDP over REST (OoP gear → central authz-resolver) | `users-info` | `POST /authz-resolver/v1/evaluate` in platform-host logs | +| 5 | OoP gear → shared Postgres (db-per-gear) | `users-info` | rows persisted in the gear's own database | +| 6 | **OoP → OoP** gear-to-gear over REST (discovered) | `api-contracts-consumer` → `api-contracts` `/charge` | provider `"method":"charge"` log line | +| 7 | Platform-plane s2s auth (K8s `TokenReview`) | every DirectoryService RPC | `platform-plane enforcement enabled`; negative: a gear without the token cannot register | +| 8 | Gear self-registration + edge route-sync | all pods | `registering gear proxy routes` in platform-host logs | +| 9 | Same software path with **no Kubernetes** | loopback run | [Local end-to-end](#local-no-kubernetes-end-to-end) | + +### Request paths + +``` +# 1) Anonymous (no token) +curl :8087/hello/v1/ping ──▶ platform-host (api-gateway edge) ──▶ hello pod + discovers via grpc-hub DirectoryService └▶ {"served_by":"hello-oop (pid 1)"} + +# 2) Authenticated + remote authz + DB +curl -H 'Authorization: Bearer ' :8087/users-info/v1/cities + └▶ edge (forwards the bearer) ──▶ users-info pod + │ 1. authenticate the bearer locally + │ 2. PEP ──▶ authz-resolver REST (back to platform-host) ──▶ allow + tenant scope + │ 3. query ──▶ shared-postgres pod (usersinfo db) + └▶ {"items":[ ... ]} + +# 3) OoP → OoP (gear-to-gear over REST) +curl -H 'Authorization: Bearer ' :8087/api-contracts-consumer/v1/charge + └▶ edge ──▶ api-contracts-consumer pod ──▶ api-contracts provider pod (REST) ──▶ {"payment_id":"...","status":"pending"} + +# Underlying every arrow above: +# • tenant-plane : a missing bearer is rejected with 401 at the edge +# (static-authn accept_all maps any non-empty bearer to the platform-root tenant) +# • platform-plane: pods ↔ DirectoryService carry x-toolkit-internal-token, validated by K8s TokenReview +# • discovery : each pod self-registers; the edge syncs its route table from grpc-hub +``` + +## Layout + +| Path | What | +|------|------| +| `apps/platform-host` | Platform-host binary crate (host mode). | +| `examples/toolkit/hello/hello` | `hello` gear + `hello-oop` OoP binary (`--features oop_module`). | +| `deploy/docker/platform-host.Dockerfile` | Platform-host image. | +| `deploy/docker/oop-gear.Dockerfile` | Generic per-gear OoP image (parameterized by build args). | +| `deploy/helm/toolkit-common` | Helm **library** chart (Deployment/Service/ConfigMap/SA + SA-token projection). | +| `deploy/helm/platform-host` | Platform-host chart. | +| `deploy/helm/hello` | `hello` OoP-gear chart. | +| `examples/toolkit/users-info/users-info` | `users-info` gear; OoP binary `users-info-oop` is a feature-gated `[[bin]]` (`--features oop_module`) in the gear crate. | +| `deploy/helm/users-info` | `users-info` OoP-gear chart (connects to the shared Postgres; can also bundle its own via `postgres.enabled`). | +| `examples/toolkit/api-contracts/api-contracts` | `api-contracts` PaymentApi REST **provider**; its OoP binary (`api-contracts-oop`) is a feature-gated `[[bin]]` (`--features oop_module`) in the gear crate. | +| `examples/toolkit/api-contracts/api-contracts-consumer` | `api-contracts-consumer`; its OoP binary (`api-contracts-consumer-oop`, feature `oop_module`) resolves `PaymentApi` from the provider **pod** over REST (OoP gear-to-gear). | +| `deploy/helm/api-contracts` | `api-contracts` provider OoP-gear chart. | +| `deploy/helm/api-contracts-consumer` | `api-contracts-consumer` OoP-gear chart. | +| `deploy/helm/postgres` | Shared PostgreSQL chart — one server, a database per gear (created by an init script). | +| `deploy/helm/toolkit-platform` | Umbrella chart (platform-host + gears) with `values-{dev,minimal,production}.yaml`. | + +## Prerequisites + +- Docker +- `minikube` + `kubectl` +- `helm` + +## 1. Build images + +```bash +# Platform-host (dev profile = faster build; drop --build-arg for optimized release). +# CARGO_FEATURES="k8s" compiles the grpc-hub inbound TokenReview validator. +DOCKER_BUILDKIT=1 docker build \ + -f deploy/docker/platform-host.Dockerfile \ + --build-arg BUILD_PROFILE=dev \ + --build-arg CARGO_FEATURES="k8s" \ + -t ghcr.io/constructorfabric/platform-host:dev . + +# hello OoP gear (generic per-gear Dockerfile) +DOCKER_BUILDKIT=1 docker build \ + -f deploy/docker/oop-gear.Dockerfile \ + --build-arg GEAR_PACKAGE=hello \ + --build-arg GEAR_BIN=hello-oop \ + --build-arg GEAR_FEATURES="oop_module,k8s-auth" \ + --build-arg GEAR_CONFIG=config/oop-hello.yaml \ + --build-arg BUILD_PROFILE=dev \ + -t ghcr.io/constructorfabric/hello:dev . + +# users-info OoP gear (authenticated + DB). The OoP binary is a feature-gated +# [[bin]] in the gear crate, so GEAR_PACKAGE is the gear crate and GEAR_FEATURES +# enables oop_module (+ k8s-auth for the platform plane). +DOCKER_BUILDKIT=1 docker build \ + -f deploy/docker/oop-gear.Dockerfile \ + --build-arg GEAR_PACKAGE=users-info \ + --build-arg GEAR_BIN=users-info-oop \ + --build-arg GEAR_FEATURES="oop_module,k8s-auth" \ + --build-arg GEAR_CONFIG=config/oop-users-info.yaml \ + --build-arg BUILD_PROFILE=dev \ + -t ghcr.io/constructorfabric/users-info:dev . + +# api-contracts PaymentApi REST PROVIDER (authenticated, no DB). The OoP binary +# is a feature-gated [[bin]] in the gear crate (no separate -oop crate), so +# GEAR_PACKAGE is the gear crate and GEAR_FEATURES enables oop_module. +DOCKER_BUILDKIT=1 docker build \ + -f deploy/docker/oop-gear.Dockerfile \ + --build-arg GEAR_PACKAGE=cf-api-contracts \ + --build-arg GEAR_BIN=api-contracts-oop \ + --build-arg GEAR_FEATURES="oop_module,k8s-auth" \ + --build-arg GEAR_CONFIG=config/oop-api-contracts.yaml \ + --build-arg BUILD_PROFILE=dev \ + -t ghcr.io/constructorfabric/api-contracts:dev . + +# api-contracts-consumer (authenticated, no DB) — calls the provider POD over REST. +# Also a feature-gated [[bin]] in the gear crate. +DOCKER_BUILDKIT=1 docker build \ + -f deploy/docker/oop-gear.Dockerfile \ + --build-arg GEAR_PACKAGE=cf-api-contracts-consumer \ + --build-arg GEAR_BIN=api-contracts-consumer-oop \ + --build-arg GEAR_FEATURES="oop_module,k8s-auth" \ + --build-arg GEAR_CONFIG=config/oop-api-contracts-consumer.yaml \ + --build-arg BUILD_PROFILE=dev \ + -t ghcr.io/constructorfabric/api-contracts-consumer:dev . + +# cluster (coordination system gear: cache / lock / leader election). Its OoP +# binary is unconditional (no oop_module feature), so GEAR_FEATURES is just +# k8s-auth. It serves its coordination gRPC plane on :50051. +DOCKER_BUILDKIT=1 docker build \ + -f deploy/docker/oop-gear.Dockerfile \ + --build-arg GEAR_PACKAGE=cf-gears-cluster \ + --build-arg GEAR_BIN=cluster-oop \ + --build-arg GEAR_FEATURES="k8s-auth" \ + --build-arg GEAR_CONFIG=config/oop-cluster.yaml \ + --build-arg BUILD_PROFILE=dev \ + -t ghcr.io/constructorfabric/cluster:dev . + +# cluster-consumer — resolves ClusterCacheV1 from the cluster pod over gRPC. +DOCKER_BUILDKIT=1 docker build \ + -f deploy/docker/oop-gear.Dockerfile \ + --build-arg GEAR_PACKAGE=cluster-consumer \ + --build-arg GEAR_BIN=cluster-consumer-oop \ + --build-arg GEAR_FEATURES="oop_module,k8s-auth" \ + --build-arg GEAR_CONFIG=config/oop-cluster-consumer.yaml \ + --build-arg BUILD_PROFILE=dev \ + -t ghcr.io/constructorfabric/cluster-consumer:dev . +``` + +## 2. Load images into the cluster + +```bash +minikube start # if not already running +minikube image load ghcr.io/constructorfabric/platform-host:dev +minikube image load ghcr.io/constructorfabric/hello:dev +minikube image load ghcr.io/constructorfabric/users-info:dev +minikube image load ghcr.io/constructorfabric/api-contracts:dev +minikube image load ghcr.io/constructorfabric/api-contracts-consumer:dev +minikube image load ghcr.io/constructorfabric/cluster:dev +minikube image load ghcr.io/constructorfabric/cluster-consumer:dev +``` + +> **Docker-driver gotcha:** `minikube image load ` may **not** overwrite an +> existing tag if a running container still references the old image (you'll see +> the gear boot the *previous* build). If you rebuild an image, either delete the +> gear's pod first (`kubectl -n cf-gears delete pod -l +> app.kubernetes.io/name=`) or load via a tarball: +> `docker save -o /tmp/img.tar && minikube image load /tmp/img.tar`, then +> `kubectl -n cf-gears rollout restart deploy/`. + +## 3. Deploy + +```bash +kubectl create namespace cf-gears +minikube addons enable ingress # nginx controller for the edge Ingress +helm dependency build deploy/helm/platform-host +helm dependency build deploy/helm/hello +helm dependency build deploy/helm/users-info +helm dependency build deploy/helm/api-contracts +helm dependency build deploy/helm/api-contracts-consumer +helm dependency update deploy/helm/toolkit-platform # packages the sub-charts + +helm upgrade --install platform deploy/helm/toolkit-platform \ + -n cf-gears \ + -f deploy/helm/toolkit-platform/values-dev.yaml \ + --timeout 240s + +kubectl -n cf-gears get pods +``` + +## 4. Smoke test (edge → OoP) + +All external traffic enters through the api-gateway edge, exposed by the +`platform-host` Ingress (`values-dev.yaml` enables it at host +`platform-host.local`, class `nginx`). Enable the controller once with +`minikube addons enable ingress`, then reach the edge without editing +`/etc/hosts` via `curl --resolve`: + +> **macOS + `--driver=docker`:** `minikube ip` (e.g. `192.168.49.2`) is not +> routable from the host, so the manual `--resolve` curls below will hang. Either +> run `minikube tunnel` in a separate terminal (the ingress then answers at +> `127.0.0.1`, so use `--resolve platform-host.local:80:127.0.0.1`), or skip the +> ingress and port-forward the edge: +> `kubectl -n cf-gears port-forward svc/platform-host 8087:8087`, then +> `curl http://127.0.0.1:8087/...`. **`oop-smoke.sh` handles this automatically** +> — it probes the Ingress and falls back to a port-forward when the node IP is not +> reachable, so `--all` passes clean on every driver. + +```bash +MIP=$(minikube ip) +BASE="http://platform-host.local" +RESOLVE="--resolve platform-host.local:80:$MIP" + +curl -s $RESOLVE $BASE/healthz # platform-host edge -> 200 +curl -s $RESOLVE $BASE/hello/v1/ping # reverse-proxied to the hello pod +# => {"message":"pong","served_by":"hello-oop (pid 1)"} + +# users-info: authenticated + remote-authz + Postgres, all through the edge. +# static-authn accept_all maps any non-empty bearer to the platform-root tenant. +TID=00000000-df51-5b42-9538-d2b56b7ee953 + +curl -s -o /dev/null -w '%{http_code}\n' \ + $RESOLVE $BASE/users-info/v1/cities # no token -> 401 + +curl -s -X POST -H 'Authorization: Bearer test-token' -H 'Content-Type: application/json' \ + -d "{\"name\":\"Tokyo\",\"country\":\"JP\",\"tenant_id\":\"$TID\"}" \ + $RESOLVE $BASE/users-info/v1/cities # -> 201 Created + +curl -s -H 'Authorization: Bearer test-token' \ + $RESOLVE $BASE/users-info/v1/cities # -> {"items":[{"name":"Tokyo",...}]} +``` + +The `$RESOLVE` / `$BASE` variables set here are reused by the commands in the +rest of this guide. + +For `hello`, `served_by` is the serving process — proof the request was proxied +across pods. For `users-info`, the `201`/`200` responses prove the full OoP +path: the edge forwarded the bearer to the `users-info` pod, which authenticated +it locally, called the central `authz-resolver` **over REST** for the PEP +decision (visible as `POST /authz-resolver/v1/evaluate` in the platform-host +logs, sourced from the users-info pod IP), and persisted to its own Postgres. + +### Full smoke test + +With the demo gears enabled (`values-dev.yaml`), the edge reverse-proxies every +authenticated route that is marked `.exposed()`: + +```bash +# Anonymous +curl $RESOLVE $BASE/hello/v1/ping + +# Authenticated (static-authn accept_all maps any non-empty bearer to the +# platform-root tenant) +TOKEN='Authorization: Bearer test-token' + +curl -H "$TOKEN" $RESOLVE $BASE/users-info/v1/users + +# And the no-token variant should be 401: +curl -s -o /dev/null -w '%{http_code}\n' $RESOLVE $BASE/users-info/v1/users +``` + +## Platform-plane auth (TokenReview) + +The two-plane model separates **tenant-plane** auth (end-user `Authorization: +Bearer` → `SecurityContext`, authenticated at each gear) from **platform-plane** +auth (service-to-service, `x-toolkit-internal-token`). This deployment enforces +the platform plane end-to-end using Kubernetes `TokenReview`. + +**How it works** + +- Each pod (platform-host + every OoP gear) mounts a **projected ServiceAccount + token** with audience `toolkit-internal` at + `/var/run/secrets/tokens/toolkit-internal/token` (`saToken.enabled` in the + charts). +- Every gRPC **caller** of the DirectoryService attaches that token: + - OoP gears via `oop_http.internal_auth: { provider: kube, token_path: ... }`. + - the edge api-gateway proxy via `gateway_proxy.internal_auth`. +- The DirectoryService **receiver** (grpc-hub, in platform-host) validates every + non-exempt RPC via the K8s `TokenReview` API: + `grpc-hub.internal_auth: { provider: kube, audiences: [toolkit-internal] }` + with `internal_auth_enforcement: required`. Health + reflection RPCs are exempt. +- `templates/rbac.yaml` in the platform-host chart binds its ServiceAccount to + the built-in `system:auth-delegator` ClusterRole so it may submit + TokenReviews. The `k8s` / `k8s-auth` cargo features compile the TokenReview + code path (see [Build images](#1-build-images)). + +**Verify enforcement** + +```bash +# Positive: platform-host logs enforcement + accepted registrations. +kubectl -n cf-gears logs deploy/platform-host | grep "platform-plane enforcement enabled" +kubectl -n cf-gears logs deploy/platform-host | grep "registering gear proxy routes" + +# Negative: a caller without a valid token is rejected. Temporarily remove a +# gear's oop_http.internal_auth (e.g. users-info) and redeploy; the gear +# cannot register and stays NotReady: +kubectl -n cf-gears logs deploy/users-info | grep "Unauthenticated" +# => "directory register_instance failed: gRPC Unauthenticated: missing internal token" +``` + +> The local loopback path below runs Profile-1-style (no projected tokens); it +> leaves `internal_auth` unset, so grpc-hub runs the pass-through layer. Use the +> `shared_secret` provider to exercise the platform plane without Kubernetes. + +## OoP gear-to-gear (REST) + +The `hello` and `users-info` paths above show OoP→**host** calls (users-info +resolves the in-host `authz-resolver` over REST). The `api-contracts` pair shows +OoP→**OoP** — one gear pod calling another gear pod over REST, discovered via the +DirectoryService: + +- **`api-contracts`** (provider pod) serves the `PaymentApi` REST contract at + `/api-contracts/v1/...` and registers its endpoint in the DirectoryService. +- **`api-contracts-consumer`** (consumer pod) exposes + `POST /api-contracts-consumer/v1/charge`. Its handler resolves `dyn PaymentApi` + from the ClientHub — wired by `#[toolkit::consumes(contract = PaymentApi, from + = "api-contracts")]` to a **directory-resolving REST client** — and forwards + the charge. The consumer binary does **not** link the provider, so the call can + only travel over REST to the provider pod. + +The consumer's `/charge` route is `.exposed()`, so a single request through the +edge exercises the whole chain — **ingress → api-gateway → consumer pod → +provider pod** (the consumer→provider hop travels OoP→OoP over REST): + +```bash +curl -s -o /dev/null -w '%{http_code}\n' \ + -X POST -H 'Content-Type: application/json' \ + -d '{"amount_cents":1000,"currency":"USD","description":"test"}' \ + $RESOLVE $BASE/api-contracts-consumer/v1/charge # no token -> 401 + +curl -s -X POST -H 'Authorization: Bearer test-token' -H 'Content-Type: application/json' \ + -d '{"amount_cents":1000,"currency":"USD","description":"test charge"}' \ + $RESOLVE $BASE/api-contracts-consumer/v1/charge # -> {"payment_id":"...","status":"pending"} +``` + +Confirm the hop crossed pods (the provider actually executed the charge): + +```bash +kubectl -n cf-gears logs deploy/api-contracts | grep '"method":"charge"' +# => "contract call started" / "contract call succeeded" service=PaymentApi method=charge +kubectl -n cf-gears logs deploy/api-contracts-consumer | grep 'dependency resolved' +# => readiness: dependency resolved dep=api-contracts (the resolving REST client) +``` + +## Cluster coordination (gRPC, DNS-derived) + +The `api-contracts` pair above is OoP→OoP over **REST**, discovered via the +DirectoryService. The `cluster` pair shows OoP→OoP over **gRPC**, discovered by +**Kubernetes DNS convention** rather than the directory: + +- **`cluster`** (system gear) serves its coordination plane (cache / lock / + leader election) on a gRPC Service at **`:50051`** and registers presence with + the DirectoryService. `values-dev` runs **3 replicas on the `postgres` backend** + (database `cluster` on the shared Postgres), so all pods share state and the + Service can load-balance across them consistently. +- **`cluster-consumer`** is a small **seat-reservation service** exposing + `reservations` / `inventory` / `status` routes (see its crate docs). It resolves + the three cluster facades from the ClientHub — remote clients the framework's + proxy-wiring phase built from `cluster-sdk`'s `ConsumerRegistration` — and uses + **all three primitives** the way ADR-002 intends: every seat transition is a + versioned `ClusterCacheV1::compare_and_swap` (the correctness gate), a per-seat + `DistributedLockV1::try_lock` damps contention (no remote I/O held), and a + `LeaderElectionV1` background sweeper (one pod at a time) reclaims expired + holds. The consumer binary does **not** link the cluster gear. + +The cluster endpoint is **not** taken from config or the directory: cluster-sdk +derives it as `cluster.{POD_NAMESPACE}.svc.cluster.local:50051` (invariant I9 — +there is no cluster-side endpoint key). Two consequences for deployment: + +- The cluster chart **must** be named `cluster` (`fullnameOverride: cluster`) and + publish `:50051` (`grpc.enabled: true`, `grpc.port: 50051`), so the derived + Service DNS resolves. `POD_NAMESPACE` is injected from the downward API by the + `toolkit-common` deployment template, so the consumer needs no endpoint config. +- On plain loopback (the `make e2e-oop` suite) that DNS name does not resolve, so + a coordination call returns a typed `503` (`ConnectionLost`) — the seam proof. + Here in Kubernetes it succeeds for real: + +```bash +curl -s "$RESOLVE" "$BASE/cluster-consumer/v1/reservations" \ + -X POST -H 'Content-Type: application/json' \ + -d '{"seat":"A12","holder":"alice"}' +# => {"seat":"A12","status":"held","holder":"alice","expires_at_ms":..., +# "version":1,"served_by":"cluster-consumer-oop (pid 1)"} + +curl -s "$RESOLVE" "$BASE/cluster-consumer/v1/inventory" +# => {"total":36,"available":35,"held":1,"booked":0, +# "available_seats":["A1","A2",...],"served_by":"cluster-consumer-oop (pid 1)"} +``` + +> All three primitives are `.scoped("cluster-consumer")`, so the cache key, lock, +> and election share one namespace — the lock/election wire name composes as +> `cluster-consumer/reservation`. A leaf name is slash-free (`[a-zA-Z0-9_-]`); the +> server validates the composed, `/`-separated name with `validate_scoped_cluster_name`. + +Platform-plane auth: cluster's grpc-hub enforces `X-ToolKit-Internal-Token` via +`TokenReview` (its chart's `rbac.yaml` binds `system:auth-delegator`); the +consumer's remote client attaches its projected SA token automatically. For a +first light-up you can drop the grpc-hub `internal_auth` block in the cluster +chart's `config.content` to run the coordination plane pass-through. + +The `postgres` backend is what makes coordination correct across the 3 replicas +(distributed locks / leader election / cache all share state through the +`cluster` database; the postgres-cluster-plugin runs its own migrations on +startup). For a single-pod, infrastructure-free run, set the `demo` profile's +`cache.provider` to `standalone` in the cluster chart's `config.content` and drop +`replicaCount` to 1 — but note standalone is in-memory per-pod and does not +coordinate across replicas. + +## Local (no Kubernetes) end-to-end + +Two processes on loopback, same software path: + +```bash +# Terminal 1 — platform-host (edge + DirectoryService on TCP :50051) +cargo run -p cf-gears-platform-host -- --config config/oop-host.yaml run + +# Terminal 2 — hello OoP gear +TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ + cargo run -p hello --features oop_module --bin hello-oop -- --config config/oop-hello.yaml + +# Terminal 2b (optional) — users-info OoP gear (authenticated + DB). +# The local config uses per-pod SQLite (no Postgres needed on loopback); +# in Kubernetes the chart points it at a Postgres pod instead. +TOOLKIT_DIRECTORY_ENDPOINT=http://127.0.0.1:50051 \ + cargo run -p users-info --features oop_module --bin users-info-oop -- --config config/oop-users-info.yaml + +# Terminal 3 — through the local edge (platform-host binds :8087) +curl http://127.0.0.1:8087/hello/v1/ping + +curl -H 'Authorization: Bearer test-token' http://127.0.0.1:8087/users-info/v1/cities +``` + +## Helm presets + +| Values file | Contents | +|-------------|----------| +| `values-dev.yaml` | platform-host + shared Postgres + 4 OoP gears (`hello`, `users-info`, `api-contracts`, `api-contracts-consumer`), `pullPolicy: Never` for locally-built images (Postgres uses the public image). Enables the `platform-host` Ingress (`platform-host.local`, class `nginx`). | +| `values-minimal.yaml` | platform-host only (no OoP gears). | +| `values-production.yaml` | Scaffold for a registry-pulled prod stack (hardening TODO). Enables the `platform-host` Ingress with a placeholder host + TLS block to fill in. | + +Only the `platform-host` chart defines an Ingress; OoP gears are reached through +its api-gateway by path prefix (ADR-0007). + +## Adding another OoP gear + +1. Give the gear an OoP binary — a feature-gated `[[bin]]` in the gear crate + (`src/main.rs` + `src/registered_gears.rs`) behind an `oop_module` feature + that enables `toolkit/bootstrap`, calling `run_oop_with_options`. This is how + every OoP gear here is shaped (`hello`, `users-info`, `api-contracts`, ...): + - **anonymous, no deps** (like `hello`): `oop_module = ["dep:tokio", + "toolkit/bootstrap"]`. + - **authenticated / with dependencies** (like `users-info`): make the + embedded tenant-plane authn stack (`authn-resolver` + `static-authn-plugin` + + `types-registry`) **optional** deps and gate them behind `oop_module`, so + the library build stays slim. `registered_gears.rs` links them with + `use as _;`. +2. Build its image via `deploy/docker/oop-gear.Dockerfile` (`GEAR_PACKAGE` = the + gear crate, `GEAR_BIN` = the OoP bin, `GEAR_FEATURES="oop_module,k8s-auth"`). +3. Copy `deploy/helm/hello` (or `deploy/helm/users-info` if it needs a DB) as a + template, adjust `service.port`, `config.content` (`oop_http.advertise_uri` + + `gears.` + any `database` block), and the image. +4. Add it to the `toolkit-platform` umbrella `Chart.yaml` dependencies + values. + +**If the gear needs a database**, don't bundle its own Postgres — reuse the +shared server: add the gear's database name to `postgres.databases` in the +umbrella values, and set the gear's `postgres` block to `enabled: false`, +`host: shared-postgres`, `database: `, `user/password: platform/...`. +The gear owns that database exclusively; it must not read another gear's tables +(use SDK contracts for cross-gear data). See the database-topology guidance in +`docs/arch/database/ADR/0001-cpt-cf-database-adr-object-namespacing.md`. + +**Authenticated gears that call the central authz-resolver** additionally need: +the gear must `#[toolkit::consumes(contract = ..., from = "authz-resolver")]` and +resolve the client lazily (e.g. `PolicyEnforcer::from_hub`), and the +platform-host's api-gateway must set `advertise_uri` to its Service DNS so its +in-process REST providers (authz-resolver) are resolvable cross-pod. + +## Cleanup + +```bash +helm -n cf-gears uninstall platform +kubectl delete namespace cf-gears +# minikube stop # or: minikube delete +``` diff --git a/deploy/docker/oop-gear.Dockerfile b/deploy/docker/oop-gear.Dockerfile new file mode 100644 index 0000000000..8bf05acc5c --- /dev/null +++ b/deploy/docker/oop-gear.Dockerfile @@ -0,0 +1,84 @@ +# Generalized multi-stage build for a single OoP gear image (OoP-10 task 10.4). +# +# Parameterized by build args so one Dockerfile produces a minimal image per +# gear. Because OoP dependency resolution goes through the DirectoryService + +# REST clients (not in-process linking), a gear image only compiles the target +# gear crate + its SDK/client deps - not the full gear set. +# +# Build the hello OoP gear: +# docker build -f deploy/docker/oop-gear.Dockerfile \ +# --build-arg GEAR_PACKAGE=hello \ +# --build-arg GEAR_BIN=hello-oop \ +# --build-arg GEAR_FEATURES=oop_module \ +# --build-arg GEAR_CONFIG=config/oop-hello.yaml \ +# -t ghcr.io/constructorfabric/hello:1.0.0 . + +# --------------------------------------------------------------------------- +# Stage 1: Builder +# --------------------------------------------------------------------------- +FROM rust:1.95.0-bookworm@sha256:6bb82db0878825e157664188b319c875de4f1fff5d70f5917b3a3f1974b472e4 AS builder + +# Cargo package (crate) name, e.g. "hello". +ARG GEAR_PACKAGE +# Binary target name within the package, e.g. "hello-oop". +ARG GEAR_BIN +# Space-separated cargo features required by the OoP binary, e.g. "oop_module". +ARG GEAR_FEATURES="" +# BUILD_PROFILE: "release" (default) or "dev" (fast compile). +ARG BUILD_PROFILE=release + +# protobuf-compiler is required by prost-build (gRPC / directory protos). +RUN apt-get update && \ + apt-get install -y --no-install-recommends cmake protobuf-compiler libprotobuf-dev && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy the full workspace context (.dockerignore trims target/, .git/, logs/). +COPY . . + +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + --mount=type=cache,target=/build/target,sharing=locked \ + set -eux; \ + RELEASE_FLAG=""; OUTPUT_DIR="debug"; \ + if [ "$BUILD_PROFILE" = "release" ]; then RELEASE_FLAG="--release"; OUTPUT_DIR="release"; fi; \ + if [ -n "$GEAR_FEATURES" ]; then \ + cargo build $RELEASE_FLAG --features "$GEAR_FEATURES" \ + --bin "$GEAR_BIN" --package "$GEAR_PACKAGE"; \ + else \ + cargo build $RELEASE_FLAG \ + --bin "$GEAR_BIN" --package "$GEAR_PACKAGE"; \ + fi; \ + cp "/build/target/$OUTPUT_DIR/$GEAR_BIN" /tmp/gear + +# --------------------------------------------------------------------------- +# Stage 2: Runtime +# --------------------------------------------------------------------------- +FROM debian:13.3-slim + +# Default gear config baked into the image; override by mounting a ConfigMap +# over /app/config/gear.yaml in Kubernetes. +ARG GEAR_CONFIG=config/oop-hello.yaml + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY --from=builder /tmp/gear /app/gear +COPY --from=builder /build/${GEAR_CONFIG} /app/config/gear.yaml + +# OoP REST + probes port (matches oop_http.listen_addr / chart service.port). +EXPOSE 9091 + +# Writable runtime state dir (non-root user has no home). +ENV APP__SERVER__HOME_DIR=/app/data + +RUN useradd -U -u 1000 appuser && \ + mkdir -p /app/data && \ + chown -R 1000:1000 /app +USER 1000 + +CMD ["/app/gear", "--config", "/app/config/gear.yaml"] diff --git a/deploy/docker/platform-host.Dockerfile b/deploy/docker/platform-host.Dockerfile new file mode 100644 index 0000000000..121b157104 --- /dev/null +++ b/deploy/docker/platform-host.Dockerfile @@ -0,0 +1,88 @@ +# Multi-stage build for the CF/Gears Platform Host image (OoP-10 task 10.3). +# +# The platform-host bundles the trust-coupled core (authz-resolver, +# tenant-resolver, resource-group, account-management) + system gears +# (gear-orchestrator, types-registry, credstore, api-gateway, grpc-hub) + +# embedded authn-resolver. See docs/arch/toolkit-oop/DESIGN.md +# "Platform Host Composition". +# +# Build (dev plugins, default): +# docker build -f deploy/docker/platform-host.Dockerfile \ +# -t ghcr.io/constructorfabric/platform-host:dev . +# +# Build (production plugins: OIDC authn + tr-authz + rg-tr, k8s auth): +# docker build -f deploy/docker/platform-host.Dockerfile \ +# --build-arg CARGO_NO_DEFAULT_FEATURES=1 \ +# --build-arg CARGO_FEATURES="prod-plugins k8s" \ +# -t ghcr.io/constructorfabric/platform-host:prod . + +# --------------------------------------------------------------------------- +# Stage 1: Builder +# --------------------------------------------------------------------------- +FROM rust:1.95.0-bookworm@sha256:6bb82db0878825e157664188b319c875de4f1fff5d70f5917b3a3f1974b472e4 AS builder + +# BUILD_PROFILE: "release" (default, optimized) or "dev" (fast compile). +ARG BUILD_PROFILE=release +# Extra cargo features to enable, space-separated (e.g. "prod-plugins k8s otel"). +ARG CARGO_FEATURES="" +# Set to a non-empty value (e.g. "1") to pass --no-default-features (drops the +# default dev-plugins preset - required when building the prod-plugins image). +ARG CARGO_NO_DEFAULT_FEATURES="" + +# protobuf-compiler is required by prost-build (gRPC / directory protos). +RUN apt-get update && \ + apt-get install -y --no-install-recommends cmake protobuf-compiler libprotobuf-dev && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Copy the full workspace context (.dockerignore trims target/, .git/, logs/). +COPY . . + +# Build the platform-host binary. BuildKit cache mounts persist the cargo +# registry + target dir across builds; the binary is copied out to /tmp because +# the target dir is a cache mount and does not survive the layer. +RUN --mount=type=cache,target=/usr/local/cargo/registry,sharing=locked \ + --mount=type=cache,target=/usr/local/cargo/git,sharing=locked \ + --mount=type=cache,target=/build/target,sharing=locked \ + set -eux; \ + RELEASE_FLAG=""; OUTPUT_DIR="debug"; \ + if [ "$BUILD_PROFILE" = "release" ]; then RELEASE_FLAG="--release"; OUTPUT_DIR="release"; fi; \ + NO_DEFAULT_FLAG=""; \ + if [ -n "$CARGO_NO_DEFAULT_FEATURES" ]; then NO_DEFAULT_FLAG="--no-default-features"; fi; \ + FEATURES_FLAG=""; \ + if [ -n "$CARGO_FEATURES" ]; then FEATURES_FLAG="--features $CARGO_FEATURES"; fi; \ + cargo build $RELEASE_FLAG $NO_DEFAULT_FLAG $FEATURES_FLAG \ + --bin platform-host --package cf-gears-platform-host; \ + cp "/build/target/$OUTPUT_DIR/platform-host" /tmp/platform-host + +# --------------------------------------------------------------------------- +# Stage 2: Runtime +# --------------------------------------------------------------------------- +FROM debian:13.3-slim + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Binary (copied via /tmp because the builder target dir is a cache mount). +COPY --from=builder /tmp/platform-host /app/platform-host +# Default config; override by mounting a ConfigMap over /app/config in k8s. +COPY --from=builder /build/config/platform-host.yaml /app/config/platform-host.yaml + +# HTTP API / probes (/healthz, /readyz, /health) served on 8087. +EXPOSE 8087 + +# Runtime state (SQLite DBs, logs) lives under a writable, container-local path +# rather than the non-root user's (absent) home dir. Overrides the config's +# `server.home_dir` via the APP__ env layer. Mount a volume here for persistence. +ENV APP__SERVER__HOME_DIR=/app/data + +RUN useradd -U -u 1000 appuser && \ + mkdir -p /app/data && \ + chown -R 1000:1000 /app +USER 1000 + +CMD ["/app/platform-host", "--config", "/app/config/platform-host.yaml", "run"] diff --git a/deploy/helm/api-contracts-consumer/Chart.yaml b/deploy/helm/api-contracts-consumer/Chart.yaml new file mode 100644 index 0000000000..86c449147b --- /dev/null +++ b/deploy/helm/api-contracts-consumer/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: api-contracts-consumer +description: >- + api-contracts-consumer - an OoP gear that resolves the PaymentApi contract from + the SEPARATE api-contracts provider pod over REST (via the DirectoryService) + and forwards charges. Demonstrates the Profile 3 out-of-process gear-to-gear + REST path. +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: toolkit-common + version: 0.1.0 + repository: "file://../toolkit-common" diff --git a/deploy/helm/api-contracts-consumer/templates/configmap.yaml b/deploy/helm/api-contracts-consumer/templates/configmap.yaml new file mode 100644 index 0000000000..dd6093f9d0 --- /dev/null +++ b/deploy/helm/api-contracts-consumer/templates/configmap.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.configmap" . }} diff --git a/deploy/helm/api-contracts-consumer/templates/deployment.yaml b/deploy/helm/api-contracts-consumer/templates/deployment.yaml new file mode 100644 index 0000000000..1101d4a29c --- /dev/null +++ b/deploy/helm/api-contracts-consumer/templates/deployment.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.deployment" . }} diff --git a/deploy/helm/api-contracts-consumer/templates/service.yaml b/deploy/helm/api-contracts-consumer/templates/service.yaml new file mode 100644 index 0000000000..a27603faad --- /dev/null +++ b/deploy/helm/api-contracts-consumer/templates/service.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.service" . }} diff --git a/deploy/helm/api-contracts-consumer/templates/serviceaccount.yaml b/deploy/helm/api-contracts-consumer/templates/serviceaccount.yaml new file mode 100644 index 0000000000..fbfe1086c3 --- /dev/null +++ b/deploy/helm/api-contracts-consumer/templates/serviceaccount.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.serviceAccount" . }} diff --git a/deploy/helm/api-contracts-consumer/values.yaml b/deploy/helm/api-contracts-consumer/values.yaml new file mode 100644 index 0000000000..31fb81ca79 --- /dev/null +++ b/deploy/helm/api-contracts-consumer/values.yaml @@ -0,0 +1,121 @@ +# Default values for the api-contracts-consumer (OoP gear) chart. + +replicaCount: 1 + +image: + repository: ghcr.io/constructorfabric/api-contracts-consumer + tag: "dev" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + automount: true + annotations: {} + +command: ["/app/gear"] +args: ["--config", "/app/config/gear.yaml"] + +service: + type: ClusterIP + # OoP REST + probes port (matches oop_http.listen_addr). + port: 9098 + +grpc: + enabled: false + port: 50051 + +# DirectoryService (platform host grpc-hub) this gear registers with. Injected +# as TOOLKIT_DIRECTORY_ENDPOINT. Overridden by the umbrella chart to match the +# platform-host Service name. +directoryEndpoint: "http://platform-host:50051" + +# SA-token projection for platform-plane auth. The projected token (audience +# `toolkit-internal`) is attached to this gear's DirectoryService calls so the +# enforced grpc-hub accepts its register/resolve RPCs. +saToken: + enabled: true + audience: toolkit-internal + mountPath: /var/run/secrets/tokens/toolkit-internal + fileName: token + expirationSeconds: 3600 + +probes: + liveness: + path: /healthz + initialDelaySeconds: 3 + periodSeconds: 10 + failureThreshold: 3 + readiness: + path: /readyz + initialDelaySeconds: 2 + periodSeconds: 5 + failureThreshold: 6 + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi + +extraEnv: [] +extraVolumes: [] +extraVolumeMounts: [] +podLabels: {} +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +nodeSelector: {} +affinity: {} +tolerations: [] + +# Gear config mounted at /app/config/gear.yaml. The consumer resolves the +# PaymentApi contract from the `api-contracts` provider pod over REST (via the +# DirectoryService), so it needs no provider endpoint here. +config: + mountPath: /app/config/gear.yaml + fileName: gear.yaml + content: | + server: + home_dir: "/app/data" + + logging: + default: + console_format: "json" + console_level: info + + oop_http: + listen_addr: "0.0.0.0:9098" + advertise_uri: "http://{{ include "toolkit-common.fullname" . }}:9098" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + token_path: /var/run/secrets/tokens/toolkit-internal/token + + gears: + api-contracts-consumer: + config: {} + + authn-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + types-registry: + config: {} diff --git a/deploy/helm/api-contracts/Chart.yaml b/deploy/helm/api-contracts/Chart.yaml new file mode 100644 index 0000000000..812da022e0 --- /dev/null +++ b/deploy/helm/api-contracts/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: api-contracts +description: >- + api-contracts - the PaymentApi REST PROVIDER OoP gear. Deploys as its own pod, + authenticates tenant-plane requests locally, and serves the PaymentApi + contract at /api-contracts/v1. Consumer pods discover and call it over REST via + the DirectoryService (Profile 3 out-of-process gear-to-gear). +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: toolkit-common + version: 0.1.0 + repository: "file://../toolkit-common" diff --git a/deploy/helm/api-contracts/templates/configmap.yaml b/deploy/helm/api-contracts/templates/configmap.yaml new file mode 100644 index 0000000000..dd6093f9d0 --- /dev/null +++ b/deploy/helm/api-contracts/templates/configmap.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.configmap" . }} diff --git a/deploy/helm/api-contracts/templates/deployment.yaml b/deploy/helm/api-contracts/templates/deployment.yaml new file mode 100644 index 0000000000..1101d4a29c --- /dev/null +++ b/deploy/helm/api-contracts/templates/deployment.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.deployment" . }} diff --git a/deploy/helm/api-contracts/templates/service.yaml b/deploy/helm/api-contracts/templates/service.yaml new file mode 100644 index 0000000000..a27603faad --- /dev/null +++ b/deploy/helm/api-contracts/templates/service.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.service" . }} diff --git a/deploy/helm/api-contracts/templates/serviceaccount.yaml b/deploy/helm/api-contracts/templates/serviceaccount.yaml new file mode 100644 index 0000000000..fbfe1086c3 --- /dev/null +++ b/deploy/helm/api-contracts/templates/serviceaccount.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.serviceAccount" . }} diff --git a/deploy/helm/api-contracts/values.yaml b/deploy/helm/api-contracts/values.yaml new file mode 100644 index 0000000000..88980eaec4 --- /dev/null +++ b/deploy/helm/api-contracts/values.yaml @@ -0,0 +1,120 @@ +# Default values for the api-contracts (PaymentApi PROVIDER OoP gear) chart. + +replicaCount: 1 + +image: + repository: ghcr.io/constructorfabric/api-contracts + tag: "dev" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + automount: true + annotations: {} + +command: ["/app/gear"] +args: ["--config", "/app/config/gear.yaml"] + +service: + type: ClusterIP + # OoP REST + probes port (matches oop_http.listen_addr). + port: 9097 + +grpc: + enabled: false + port: 50051 + +# DirectoryService (platform host grpc-hub) this gear registers with. Injected +# as TOOLKIT_DIRECTORY_ENDPOINT. Overridden by the umbrella chart to match the +# platform-host Service name. +directoryEndpoint: "http://platform-host:50051" + +# SA-token projection for platform-plane auth. The projected token (audience +# `toolkit-internal`) is attached to this gear's DirectoryService calls so the +# enforced grpc-hub accepts its register/resolve RPCs. +saToken: + enabled: true + audience: toolkit-internal + mountPath: /var/run/secrets/tokens/toolkit-internal + fileName: token + expirationSeconds: 3600 + +probes: + liveness: + path: /healthz + initialDelaySeconds: 3 + periodSeconds: 10 + failureThreshold: 3 + readiness: + path: /readyz + initialDelaySeconds: 2 + periodSeconds: 5 + failureThreshold: 6 + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi + +extraEnv: [] +extraVolumes: [] +extraVolumeMounts: [] +podLabels: {} +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +nodeSelector: {} +affinity: {} +tolerations: [] + +# Gear config mounted at /app/config/gear.yaml. advertise_uri uses this gear's +# in-cluster Service DNS so consumer pods can resolve and call it over REST. +config: + mountPath: /app/config/gear.yaml + fileName: gear.yaml + content: | + server: + home_dir: "/app/data" + + logging: + default: + console_format: "json" + console_level: info + + oop_http: + listen_addr: "0.0.0.0:9097" + advertise_uri: "http://{{ include "toolkit-common.fullname" . }}:9097" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + token_path: /var/run/secrets/tokens/toolkit-internal/token + + gears: + api-contracts: + config: {} + + authn-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + types-registry: + config: {} diff --git a/deploy/helm/cluster-consumer/Chart.yaml b/deploy/helm/cluster-consumer/Chart.yaml new file mode 100644 index 0000000000..8b539a8845 --- /dev/null +++ b/deploy/helm/cluster-consumer/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: cluster-consumer +description: >- + cluster-consumer - a demo gear that resolves ClusterCacheV1 from the cluster + pod over gRPC and does a cache round-trip. Its `.exposed()` REST route is + reverse-proxied by the api-gateway edge. Demonstrates a Profile 3 consumer of + the cluster coordination plane. +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: toolkit-common + version: 0.1.0 + repository: "file://../toolkit-common" diff --git a/deploy/helm/cluster-consumer/templates/configmap.yaml b/deploy/helm/cluster-consumer/templates/configmap.yaml new file mode 100644 index 0000000000..dd6093f9d0 --- /dev/null +++ b/deploy/helm/cluster-consumer/templates/configmap.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.configmap" . }} diff --git a/deploy/helm/cluster-consumer/templates/deployment.yaml b/deploy/helm/cluster-consumer/templates/deployment.yaml new file mode 100644 index 0000000000..1101d4a29c --- /dev/null +++ b/deploy/helm/cluster-consumer/templates/deployment.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.deployment" . }} diff --git a/deploy/helm/cluster-consumer/templates/service.yaml b/deploy/helm/cluster-consumer/templates/service.yaml new file mode 100644 index 0000000000..a27603faad --- /dev/null +++ b/deploy/helm/cluster-consumer/templates/service.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.service" . }} diff --git a/deploy/helm/cluster-consumer/templates/serviceaccount.yaml b/deploy/helm/cluster-consumer/templates/serviceaccount.yaml new file mode 100644 index 0000000000..fbfe1086c3 --- /dev/null +++ b/deploy/helm/cluster-consumer/templates/serviceaccount.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.serviceAccount" . }} diff --git a/deploy/helm/cluster-consumer/values.yaml b/deploy/helm/cluster-consumer/values.yaml new file mode 100644 index 0000000000..902543e924 --- /dev/null +++ b/deploy/helm/cluster-consumer/values.yaml @@ -0,0 +1,109 @@ +# Default values for the cluster-consumer (OoP demo gear) chart. + +replicaCount: 1 + +image: + repository: ghcr.io/constructorfabric/cluster-consumer + tag: "dev" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + automount: true + annotations: {} + +command: ["/app/gear"] +args: ["--config", "/app/config/gear.yaml"] + +service: + type: ClusterIP + # OoP REST + probes port (matches oop_http.listen_addr). + port: 9093 + +grpc: + enabled: false + port: 50051 + +# DirectoryService (platform host grpc-hub) this gear registers with. The +# consumer does NOT use the directory to find cluster — it derives cluster's gRPC +# endpoint by DNS convention (cluster.{POD_NAMESPACE}.svc.cluster.local:50051). +# POD_NAMESPACE is injected automatically by the toolkit-common deployment +# template from the downward API, so no extra config is needed here. +directoryEndpoint: "http://platform-host:50051" + +# SA-token projection (audience toolkit-internal): the consumer's remote cluster +# client attaches it as X-ToolKit-Internal-Token on every coordination call +# (cluster's grpc-hub validates it via TokenReview). +saToken: + enabled: true + audience: toolkit-internal + mountPath: /var/run/secrets/tokens/toolkit-internal + fileName: token + expirationSeconds: 3600 + +probes: + liveness: + path: /healthz + initialDelaySeconds: 3 + periodSeconds: 10 + failureThreshold: 3 + readiness: + path: /readyz + initialDelaySeconds: 2 + periodSeconds: 5 + failureThreshold: 6 + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi + +extraEnv: [] +extraVolumes: [] +extraVolumeMounts: [] +podLabels: {} +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +nodeSelector: {} +affinity: {} +tolerations: [] + +# Gear config mounted at /app/config/gear.yaml. advertise_uri uses this gear's +# in-cluster Service DNS so the edge (in the platform-host pod) can reach it. +config: + mountPath: /app/config/gear.yaml + fileName: gear.yaml + content: | + server: + home_dir: "/app/data" + + logging: + default: + console_format: "json" + console_level: info + + oop_http: + listen_addr: "0.0.0.0:9093" + advertise_uri: "http://{{ include "toolkit-common.fullname" . }}:9093" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + token_path: /var/run/secrets/tokens/toolkit-internal/token + + gears: + cluster-consumer: + config: {} diff --git a/deploy/helm/cluster/Chart.yaml b/deploy/helm/cluster/Chart.yaml new file mode 100644 index 0000000000..1edc50f386 --- /dev/null +++ b/deploy/helm/cluster/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: cluster +description: >- + cluster - the coordination system gear (cache / distributed lock / leader + election) as its own pod. Serves its gRPC coordination plane on :50051 (the + port consumers derive by DNS convention) and registers presence with the + platform host's DirectoryService. Deploys the Profile 3 out-of-process path. +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: toolkit-common + version: 0.1.0 + repository: "file://../toolkit-common" diff --git a/deploy/helm/cluster/templates/configmap.yaml b/deploy/helm/cluster/templates/configmap.yaml new file mode 100644 index 0000000000..dd6093f9d0 --- /dev/null +++ b/deploy/helm/cluster/templates/configmap.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.configmap" . }} diff --git a/deploy/helm/cluster/templates/deployment.yaml b/deploy/helm/cluster/templates/deployment.yaml new file mode 100644 index 0000000000..1101d4a29c --- /dev/null +++ b/deploy/helm/cluster/templates/deployment.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.deployment" . }} diff --git a/deploy/helm/cluster/templates/rbac.yaml b/deploy/helm/cluster/templates/rbac.yaml new file mode 100644 index 0000000000..82e13811ff --- /dev/null +++ b/deploy/helm/cluster/templates/rbac.yaml @@ -0,0 +1,31 @@ +{{- /* +Platform-plane TokenReview RBAC. + +cluster's grpc-hub enforces inbound platform-plane credentials +(`internal_auth_enforcement: required`) by submitting a Kubernetes TokenReview. +The reviewer identity is this release's ServiceAccount (the pod's default mounted +token), which must be allowed to create TokenReviews. The built-in +`system:auth-delegator` ClusterRole grants exactly `create` on +`tokenreviews`/`subjectaccessreviews`. + +Rendered only when platform-plane auth is active (saToken.enabled) and a +dedicated ServiceAccount is created. If you disable enforcement on cluster's +grpc-hub (drop `internal_auth`/set enforcement to a pass-through) for a first +light-up, this binding is harmless but unnecessary. +*/}} +{{- if and .Values.saToken.enabled .Values.serviceAccount.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "toolkit-common.fullname" . }}-auth-delegator + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: + - kind: ServiceAccount + name: {{ include "toolkit-common.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/helm/cluster/templates/service.yaml b/deploy/helm/cluster/templates/service.yaml new file mode 100644 index 0000000000..a27603faad --- /dev/null +++ b/deploy/helm/cluster/templates/service.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.service" . }} diff --git a/deploy/helm/cluster/templates/serviceaccount.yaml b/deploy/helm/cluster/templates/serviceaccount.yaml new file mode 100644 index 0000000000..fbfe1086c3 --- /dev/null +++ b/deploy/helm/cluster/templates/serviceaccount.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.serviceAccount" . }} diff --git a/deploy/helm/cluster/values.yaml b/deploy/helm/cluster/values.yaml new file mode 100644 index 0000000000..45c7fc054e --- /dev/null +++ b/deploy/helm/cluster/values.yaml @@ -0,0 +1,155 @@ +# Default values for the cluster (OoP system gear) chart. + +replicaCount: 1 + +image: + repository: ghcr.io/constructorfabric/cluster + tag: "dev" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + automount: true + annotations: {} + +command: ["/app/gear"] +args: ["--config", "/app/config/gear.yaml"] + +service: + type: ClusterIP + # OoP HTTP probe port (matches oop_http.listen_addr). + port: 9092 + +# cluster's coordination gRPC plane. A consumer derives its endpoint by DNS +# convention as cluster.{namespace}.svc.cluster.local:50051 (cluster_sdk::wiring, +# CLUSTER_GRPC_PORT) — so this Service MUST publish :50051 and this chart MUST be +# named `cluster` (set fullnameOverride: cluster). The container binds it via +# gears.grpc-hub.config.listen_addr below. +grpc: + enabled: true + port: 50051 + +# DirectoryService (platform host grpc-hub) this gear registers presence with. +directoryEndpoint: "http://platform-host:50051" + +# Backing store for the `demo` profile. The postgres backend is what makes +# coordination correct across replicas (all pods share state through Postgres), +# so it is the default here and pairs with replicaCount > 1. Point this at the +# shared Postgres and add `cluster` to the umbrella `postgres.databases` list so +# the database is created. The postgres-cluster-plugin runs its own migrations on +# startup. For a single-pod, infrastructure-free run, set provider to standalone +# in config.content below instead (in-memory; does NOT coordinate across pods). +postgres: + host: shared-postgres + user: platform + password: "platform-demo-pw" + database: cluster + +# SA-token projection for platform-plane auth (audience toolkit-internal), +# mounted so the gear can (a) present it when registering with the platform-host +# DirectoryService and (b) validate inbound coordination calls via TokenReview. +saToken: + enabled: true + audience: toolkit-internal + mountPath: /var/run/secrets/tokens/toolkit-internal + fileName: token + expirationSeconds: 3600 + +probes: + liveness: + path: /healthz + initialDelaySeconds: 3 + periodSeconds: 10 + failureThreshold: 3 + readiness: + path: /readyz + initialDelaySeconds: 2 + periodSeconds: 5 + failureThreshold: 6 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 512Mi + +extraEnv: [] +extraVolumes: [] +extraVolumeMounts: [] +podLabels: {} +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +nodeSelector: {} +affinity: {} +tolerations: [] + +# Gear config mounted at /app/config/gear.yaml (rendered with `tpl`, so the Helm +# expressions below are evaluated). +# +# * Probes bind 0.0.0.0:9092 (http). advertise_uri uses the in-cluster Service +# DNS so the platform-host edge can reach it. +# * cluster's grpc-hub binds 0.0.0.0:50051 (the coordination plane) and enforces +# inbound X-ToolKit-Internal-Token via TokenReview. To disable enforcement for +# a first light-up, drop the grpc-hub `internal_auth` block and the +# `internal_auth_enforcement` key (the rbac.yaml binding then goes unused). +# * One profile named `demo`, matched by the consumer's DemoProfile marker, on +# the postgres backend (shared state across replicas). See the `postgres` +# value block above; swap to `{ provider: standalone }` for a single-pod run. +config: + mountPath: /app/config/gear.yaml + fileName: gear.yaml + content: | + server: + home_dir: "/app/data" + + logging: + default: + console_format: "json" + console_level: info + + oop_http: + listen_addr: "0.0.0.0:9092" + advertise_uri: "http://{{ include "toolkit-common.fullname" . }}:9092" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + token_path: /var/run/secrets/tokens/toolkit-internal/token + + gears: + grpc-hub: + config: + listen_addr: "0.0.0.0:50051" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + internal_auth_enforcement: required + cluster: + config: + profiles: + demo: + # Postgres backend: correct across replicas (shared state). For a + # single-pod infra-free run, replace with: cache: { provider: standalone } + cache: + provider: postgres + connection_string: "postgres://{{ .Values.postgres.user }}:{{ .Values.postgres.password }}@{{ .Values.postgres.host }}:5432/{{ .Values.postgres.database }}" + # Native Postgres distributed lock (pg_advisory_lock + the + # `cluster_lock` table). Declaring it explicitly uses + # `PostgresLockProvider` instead of the SDK CAS-over-cache default. + lock: + provider: postgres + connection_string: "postgres://{{ .Values.postgres.user }}:{{ .Values.postgres.password }}@{{ .Values.postgres.host }}:5432/{{ .Values.postgres.database }}" + # leader_election is omitted: no native provider exists, so it uses + # the SDK CAS-based default layered over the postgres cache above. diff --git a/deploy/helm/hello/Chart.yaml b/deploy/helm/hello/Chart.yaml new file mode 100644 index 0000000000..7e5ec9a7a4 --- /dev/null +++ b/deploy/helm/hello/Chart.yaml @@ -0,0 +1,13 @@ +apiVersion: v2 +name: hello +description: >- + Hello - a minimal REST OoP demo gear. Deploys as its own pod, registers its + REST endpoint with the platform host's DirectoryService, and is reverse-proxied + by the api-gateway edge. Demonstrates the Profile 3 out-of-process gear path. +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: toolkit-common + version: 0.1.0 + repository: "file://../toolkit-common" diff --git a/deploy/helm/hello/templates/configmap.yaml b/deploy/helm/hello/templates/configmap.yaml new file mode 100644 index 0000000000..dd6093f9d0 --- /dev/null +++ b/deploy/helm/hello/templates/configmap.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.configmap" . }} diff --git a/deploy/helm/hello/templates/deployment.yaml b/deploy/helm/hello/templates/deployment.yaml new file mode 100644 index 0000000000..1101d4a29c --- /dev/null +++ b/deploy/helm/hello/templates/deployment.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.deployment" . }} diff --git a/deploy/helm/hello/templates/service.yaml b/deploy/helm/hello/templates/service.yaml new file mode 100644 index 0000000000..a27603faad --- /dev/null +++ b/deploy/helm/hello/templates/service.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.service" . }} diff --git a/deploy/helm/hello/templates/serviceaccount.yaml b/deploy/helm/hello/templates/serviceaccount.yaml new file mode 100644 index 0000000000..fbfe1086c3 --- /dev/null +++ b/deploy/helm/hello/templates/serviceaccount.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.serviceAccount" . }} diff --git a/deploy/helm/hello/values.yaml b/deploy/helm/hello/values.yaml new file mode 100644 index 0000000000..2f684afcc0 --- /dev/null +++ b/deploy/helm/hello/values.yaml @@ -0,0 +1,107 @@ +# Default values for the hello (OoP gear) chart. + +replicaCount: 1 + +image: + repository: ghcr.io/constructorfabric/hello + tag: "dev" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + automount: true + annotations: {} + +command: ["/app/gear"] +args: ["--config", "/app/config/gear.yaml"] + +service: + type: ClusterIP + # OoP REST + probes port (matches oop_http.listen_addr). + port: 9091 + +grpc: + enabled: false + port: 50051 + +# DirectoryService (platform host grpc-hub) this gear registers with. Injected +# as TOOLKIT_DIRECTORY_ENDPOINT. Overridden by the umbrella chart to match the +# platform-host Service name. +directoryEndpoint: "http://platform-host:50051" + +# SA-token projection for platform-plane auth. The projected token (audience +# `toolkit-internal`) is mounted so the gear can present it on system calls when +# oop_http.internal_auth is configured (TokenReview-validated by the receiver). +saToken: + enabled: true + audience: toolkit-internal + mountPath: /var/run/secrets/tokens/toolkit-internal + fileName: token + expirationSeconds: 3600 + +probes: + liveness: + path: /healthz + initialDelaySeconds: 3 + periodSeconds: 10 + failureThreshold: 3 + readiness: + path: /readyz + initialDelaySeconds: 2 + periodSeconds: 5 + failureThreshold: 6 + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + memory: 256Mi + +extraEnv: [] +extraVolumes: [] +extraVolumeMounts: [] +podLabels: {} +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +nodeSelector: {} +affinity: {} +tolerations: [] + +# Gear config mounted at /app/config/gear.yaml. advertise_uri uses this gear's +# in-cluster Service DNS so the edge (in the platform-host pod) can reach it. +config: + mountPath: /app/config/gear.yaml + fileName: gear.yaml + content: | + server: + home_dir: "/app/data" + + logging: + default: + console_format: "json" + console_level: info + + oop_http: + listen_addr: "0.0.0.0:9091" + advertise_uri: "http://{{ include "toolkit-common.fullname" . }}:9091" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + token_path: /var/run/secrets/tokens/toolkit-internal/token + + gears: + hello: + config: {} diff --git a/deploy/helm/platform-host/Chart.yaml b/deploy/helm/platform-host/Chart.yaml new file mode 100644 index 0000000000..399b9236a3 --- /dev/null +++ b/deploy/helm/platform-host/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: platform-host +description: >- + CF/Gears Platform Host - the trust-coupled core (authz-resolver, + tenant-resolver, resource-group, account-management) plus system gears + (gear-orchestrator, types-registry, credstore, api-gateway, grpc-hub) and the + embedded authn-resolver, deployed as a single pod. Serves the edge api-gateway + and the DirectoryService (grpc-hub) that OoP gear pods register with. +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: toolkit-common + version: 0.1.0 + repository: "file://../toolkit-common" diff --git a/deploy/helm/platform-host/templates/configmap.yaml b/deploy/helm/platform-host/templates/configmap.yaml new file mode 100644 index 0000000000..dd6093f9d0 --- /dev/null +++ b/deploy/helm/platform-host/templates/configmap.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.configmap" . }} diff --git a/deploy/helm/platform-host/templates/deployment.yaml b/deploy/helm/platform-host/templates/deployment.yaml new file mode 100644 index 0000000000..1101d4a29c --- /dev/null +++ b/deploy/helm/platform-host/templates/deployment.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.deployment" . }} diff --git a/deploy/helm/platform-host/templates/ingress.yaml b/deploy/helm/platform-host/templates/ingress.yaml new file mode 100644 index 0000000000..06bbf3550e --- /dev/null +++ b/deploy/helm/platform-host/templates/ingress.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.ingress" . }} diff --git a/deploy/helm/platform-host/templates/rbac.yaml b/deploy/helm/platform-host/templates/rbac.yaml new file mode 100644 index 0000000000..fb12fcf141 --- /dev/null +++ b/deploy/helm/platform-host/templates/rbac.yaml @@ -0,0 +1,28 @@ +{{- /* +Platform-plane TokenReview RBAC. + +grpc-hub validates inbound platform-plane credentials by submitting a +Kubernetes TokenReview. The reviewer identity is this release's ServiceAccount +(the pod's default mounted token), which must be allowed to create +TokenReviews. The built-in `system:auth-delegator` ClusterRole grants exactly +`create` on `tokenreviews`/`subjectaccessreviews`. + +Rendered only when platform-plane auth is active (saToken.enabled) and a +dedicated ServiceAccount is created. +*/}} +{{- if and .Values.saToken.enabled .Values.serviceAccount.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "toolkit-common.fullname" . }}-auth-delegator + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator +subjects: + - kind: ServiceAccount + name: {{ include "toolkit-common.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/helm/platform-host/templates/service.yaml b/deploy/helm/platform-host/templates/service.yaml new file mode 100644 index 0000000000..a27603faad --- /dev/null +++ b/deploy/helm/platform-host/templates/service.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.service" . }} diff --git a/deploy/helm/platform-host/templates/serviceaccount.yaml b/deploy/helm/platform-host/templates/serviceaccount.yaml new file mode 100644 index 0000000000..fbfe1086c3 --- /dev/null +++ b/deploy/helm/platform-host/templates/serviceaccount.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.serviceAccount" . }} diff --git a/deploy/helm/platform-host/values.yaml b/deploy/helm/platform-host/values.yaml new file mode 100644 index 0000000000..ff786ca1d8 --- /dev/null +++ b/deploy/helm/platform-host/values.yaml @@ -0,0 +1,306 @@ +# Default values for the platform-host chart. + +replicaCount: 1 + +image: + repository: ghcr.io/constructorfabric/platform-host + # Defaults to the chart's appVersion when unset. + tag: "dev" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + automount: true + annotations: {} + +# Override the container command/args to point at the Helm-mounted config. +command: ["/app/platform-host"] +args: ["--config", "/app/config/platform-host.yaml", "run"] + +service: + type: ClusterIP + # HTTP edge (api-gateway) + probes. + port: 8087 + +# gRPC DirectoryService (grpc-hub) exposed so OoP gear pods can register/resolve +# across pods via this Service's DNS name. +grpc: + enabled: true + port: 50051 + +# Optional external HTTP entry point in front of the api-gateway edge. +# Disabled by default; the umbrella dev/prod presets enable it. OoP gears are +# reached through this gateway by path prefix, not via their own Ingress +# (ADR-0007). +ingress: + enabled: false + className: "" + # When fronted by nginx-ingress, the edge benefits from controller tuning so + # it can proxy streaming responses and larger request bodies without the + # default 60s stream timeout / 1MB body cap. The umbrella dev/prod presets set + # these; enable them here if using this chart standalone: + # nginx.ingress.kubernetes.io/proxy-buffering: "off" + # nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + # nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + # nginx.ingress.kubernetes.io/proxy-body-size: "64m" + annotations: {} + hosts: + - host: platform-host.local + paths: + - path: / + pathType: Prefix + tls: [] + +# The platform-host is the DirectoryService, so it does not itself dial one. +directoryEndpoint: "" + +# SA-token projection: the platform-host is BOTH the receiver/validator of +# platform-plane tokens (grpc-hub TokenReview) AND a caller — its api-gateway +# reverse-proxy polls the DirectoryService over gRPC and must present its own +# projected token. A ClusterRoleBinding to `system:auth-delegator` (see +# templates/rbac.yaml) grants this SA permission to submit TokenReviews. +saToken: + enabled: true + audience: toolkit-internal + mountPath: /var/run/secrets/tokens/toolkit-internal + fileName: token + expirationSeconds: 3600 + +# Shared Postgres connection for platform-host's own trust-coupled-core gears +# (account-management, resource-group, credstore). Each gets its own database +# on the shared server (see the umbrella's `postgres.databases`), not a shared +# schema. Points at the `postgres` chart deployed alongside this one. +postgres: + host: "shared-postgres" + user: platform + password: "platform-demo-pw" + databases: + accountManagement: accountmanagement + resourceGroup: resourcegroup + credstore: credstore + +probes: + liveness: + path: /healthz + initialDelaySeconds: 5 + periodSeconds: 15 + failureThreshold: 3 + readiness: + path: /readyz + initialDelaySeconds: 3 + periodSeconds: 5 + failureThreshold: 6 + +resources: + requests: + cpu: 250m + memory: 512Mi + limits: + memory: 2Gi + +extraEnv: [] +extraVolumes: [] +extraVolumeMounts: [] +podLabels: {} +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +nodeSelector: {} +affinity: {} +tolerations: [] + +# --------------------------------------------------------------------------- +# Gear configuration mounted at /app/config/platform-host.yaml. +# +# This is the Kubernetes variant of config/platform-host.yaml: +# * grpc-hub listens on TCP 0.0.0.0:50051 (reachable cross-pod via Service DNS) +# instead of a Unix domain socket. +# * api-gateway runs the embedded-edge reverse proxy (gateway_proxy), polling +# the in-pod DirectoryService and proxying external requests to OoP gears. +# * tenant-plane auth is enforced at the edge: `.authenticated()` routes +# require a bearer token, validated by the authn-resolver + static-authn +# plugin (accept_all maps any non-empty bearer to the platform-root tenant). +# --------------------------------------------------------------------------- +config: + mountPath: /app/config/platform-host.yaml + fileName: platform-host.yaml + content: | + server: + home_dir: "/app/data" + + database: + servers: + account_management_pg: + engine: "postgres" + host: "{{ .Values.postgres.host }}" + port: 5432 + user: {{ .Values.postgres.user | quote }} + password: {{ .Values.postgres.password | quote }} + dbname: {{ .Values.postgres.databases.accountManagement | quote }} + pool: + max_conns: 10 + acquire_timeout: "30s" + resource_group_pg: + engine: "postgres" + host: "{{ .Values.postgres.host }}" + port: 5432 + user: {{ .Values.postgres.user | quote }} + password: {{ .Values.postgres.password | quote }} + dbname: {{ .Values.postgres.databases.resourceGroup | quote }} + pool: + max_conns: 10 + acquire_timeout: "30s" + credstore_pg: + engine: "postgres" + host: "{{ .Values.postgres.host }}" + port: 5432 + user: {{ .Values.postgres.user | quote }} + password: {{ .Values.postgres.password | quote }} + dbname: {{ .Values.postgres.databases.credstore | quote }} + pool: + max_conns: 10 + acquire_timeout: "30s" + + logging: + default: + console_format: "json" + console_level: info + file_level: info + + gears: + api-gateway: + config: + bind_addr: "0.0.0.0:8087" + # In-cluster Service DNS other pods use to reach this gateway. The pod + # binds 0.0.0.0, so this must be set explicitly: it is advertised in + # the DirectoryService for in-process REST providers (e.g. + # authz-resolver) so out-of-process gears (users-info) can resolve and + # call them over REST. + advertise_uri: "http://platform-host:8087" + enable_docs: true + cors_enabled: false + openapi: + title: "CF/Gears Platform Host API" + version: "0.1.0" + description: "CF/Gears Platform Host API Documentation" + defaults: + body_limit_bytes: 64000000 + rate_limit: + rps: 1000 + burst: 200 + in_flight: 64 + auth_disabled: false + require_auth_by_default: true + gateway_proxy: + enabled: true + directory_endpoint: "http://127.0.0.1:50051" + sync_interval_secs: 5 + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + token_path: /var/run/secrets/tokens/toolkit-internal/token + + gear-orchestrator: + config: {} + + grpc-hub: + config: + listen_addr: "0.0.0.0:50051" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + internal_auth_enforcement: required + + authn-resolver: + config: + vendor: "constructorfabric" + + authz-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + static-authz-plugin: + config: + vendor: "constructorfabric" + priority: 100 + + tenant-resolver: + config: + vendor: "constructorfabric" + + static-tr-plugin: + config: + vendor: "constructorfabric" + priority: 20 + tenants: + - id: "00000000-df51-5b42-9538-d2b56b7ee953" + name: "platform-root" + parent_id: null + status: active + + types-registry: + config: + entities: + - $id: "gts://gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + $schema: "http://json-schema.org/draft-07/schema#" + description: "Platform-root tenant type (no parents)." + type: "object" + x-gts-traits: + allowed_parent_types: [] + idp_provisioning: false + - $id: "gts://gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~" + $schema: "http://json-schema.org/draft-07/schema#" + description: "Customer tenant under the platform root." + type: "object" + x-gts-traits: + allowed_parent_types: + - "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + - "gts.cf.core.am.tenant_type.v1~cf.core.am.customer.v1~" + idp_provisioning: false + + account-management: + database: + server: "account_management_pg" + config: + bootstrap: + root_id: "00000000-df51-5b42-9538-d2b56b7ee953" + root_name: "platform-root" + root_tenant_type: "gts.cf.core.am.tenant_type.v1~cf.core.am.platform.v1~" + idp_wait_timeout: "5m" + idp_retry_backoff_initial: "2s" + idp_retry_backoff_max: "30s" + strict: false + idp: + required: true + + resource-group: + database: + server: "resource_group_pg" + config: {} + + credstore: + database: + server: "credstore_pg" + config: + vendor: "constructorfabric" + + static-credstore-plugin: + config: + secrets: [] diff --git a/deploy/helm/postgres/Chart.yaml b/deploy/helm/postgres/Chart.yaml new file mode 100644 index 0000000000..a7d49d13b0 --- /dev/null +++ b/deploy/helm/postgres/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: postgres +description: >- + Shared, single-instance PostgreSQL for the CF/Gears demo. One Postgres + Deployment/Service serving a database-per-gear: each OoP gear owns its own + logical database on this shared server (never shared tables). For anything + beyond a demo, replace with a managed/HA PostgreSQL. +type: application +version: 0.1.0 +appVersion: "17" diff --git a/deploy/helm/postgres/templates/_helpers.tpl b/deploy/helm/postgres/templates/_helpers.tpl new file mode 100644 index 0000000000..36f5024a78 --- /dev/null +++ b/deploy/helm/postgres/templates/_helpers.tpl @@ -0,0 +1,37 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "postgres.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Fully qualified app name. Honors fullnameOverride so the Service DNS is +predictable (gears reference it by name), e.g. "shared-postgres". +*/}} +{{- define "postgres.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{- define "postgres.labels" -}} +helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{ include "postgres.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{- define "postgres.selectorLabels" -}} +app.kubernetes.io/name: {{ include "postgres.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/deploy/helm/postgres/templates/deployment.yaml b/deploy/helm/postgres/templates/deployment.yaml new file mode 100644 index 0000000000..5f634de11c --- /dev/null +++ b/deploy/helm/postgres/templates/deployment.yaml @@ -0,0 +1,87 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "postgres.fullname" . }} + labels: + {{- include "postgres.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "postgres.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "postgres.selectorLabels" . | nindent 8 }} + annotations: + # Re-run init when the database list changes (only takes effect on a + # fresh data dir, but forces a pod roll so the new ConfigMap is mounted). + checksum/init: {{ include (print $.Template.BasePath "/init-configmap.yaml") . | sha256sum }} + spec: + securityContext: + runAsNonRoot: true + runAsUser: 70 + fsGroup: 70 + containers: + - name: postgres + image: {{ .Values.image | quote }} + imagePullPolicy: {{ .Values.pullPolicy }} + ports: + - name: postgres + containerPort: 5432 + protocol: TCP + envFrom: + - secretRef: + name: {{ include "postgres.fullname" . }} + env: + # Keep the data subdir off the mount root so an emptyDir (or PVC) + # mounted at /var/lib/postgresql/data initializes cleanly. + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + readinessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.auth.username | quote }}, "-d", {{ .Values.auth.maintenanceDb | quote }}] + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 12 + livenessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.auth.username | quote }}, "-d", {{ .Values.auth.maintenanceDb | quote }}] + initialDelaySeconds: 15 + periodSeconds: 15 + failureThreshold: 6 + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + - name: init + mountPath: /docker-entrypoint-initdb.d + readOnly: true + volumes: + - name: init + configMap: + name: {{ include "postgres.fullname" . }}-init + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "postgres.fullname" . }} + {{- else }} + emptyDir: {} + {{- end }} +{{- if .Values.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "postgres.fullname" . }} + labels: + {{- include "postgres.labels" . | nindent 4 }} +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- end }} diff --git a/deploy/helm/postgres/templates/init-configmap.yaml b/deploy/helm/postgres/templates/init-configmap.yaml new file mode 100644 index 0000000000..c2e297cb0a --- /dev/null +++ b/deploy/helm/postgres/templates/init-configmap.yaml @@ -0,0 +1,20 @@ +{{/* +Init SQL runs ONLY on first boot (empty data dir), executed by the postgres +image's docker-entrypoint from /docker-entrypoint-initdb.d. It idempotently +creates one database per gear ("database per service") on this shared server. +`\gexec` is a psql meta-command; CREATE DATABASE cannot run inside a DO block or +a transaction, so we generate + execute it via \gexec instead. +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "postgres.fullname" . }}-init + labels: + {{- include "postgres.labels" . | nindent 4 }} +data: + 10-databases.sql: | + -- Create a dedicated database for each gear if it does not already exist. + {{- range .Values.databases }} + SELECT format('CREATE DATABASE %I OWNER %I', '{{ . }}', '{{ $.Values.auth.username }}') + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '{{ . }}')\gexec + {{- end }} diff --git a/deploy/helm/postgres/templates/secret.yaml b/deploy/helm/postgres/templates/secret.yaml new file mode 100644 index 0000000000..ec705c2e6e --- /dev/null +++ b/deploy/helm/postgres/templates/secret.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "postgres.fullname" . }} + labels: + {{- include "postgres.labels" . | nindent 4 }} +type: Opaque +stringData: + # Bootstrap superuser (owns every per-gear database created below). + POSTGRES_USER: {{ .Values.auth.username | quote }} + POSTGRES_PASSWORD: {{ .Values.auth.password | quote }} + # Maintenance database only; per-gear databases are created by the init script. + POSTGRES_DB: {{ .Values.auth.maintenanceDb | quote }} diff --git a/deploy/helm/postgres/templates/service.yaml b/deploy/helm/postgres/templates/service.yaml new file mode 100644 index 0000000000..38b1111d24 --- /dev/null +++ b/deploy/helm/postgres/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "postgres.fullname" . }} + labels: + {{- include "postgres.labels" . | nindent 4 }} +spec: + type: ClusterIP + selector: + {{- include "postgres.selectorLabels" . | nindent 4 }} + ports: + - name: postgres + port: {{ .Values.service.port }} + targetPort: postgres + protocol: TCP diff --git a/deploy/helm/postgres/values.yaml b/deploy/helm/postgres/values.yaml new file mode 100644 index 0000000000..9f763b8dd8 --- /dev/null +++ b/deploy/helm/postgres/values.yaml @@ -0,0 +1,41 @@ +# Shared PostgreSQL for the CF/Gears demo. +# +# One Postgres server, a database per gear. Each OoP gear connects with the +# shared bootstrap role but to its OWN database (see `databases` below); gears +# never share tables (cross-gear reads go through SDK contracts, not SQL). +# +# NOTE: this is a demo-grade, single-replica Postgres with a static credential +# and (by default) ephemeral storage. Replace with a managed/HA database and a +# real secret for anything beyond minikube. + +nameOverride: "" +# Stable Service DNS name that gears reference in their DB config. +fullnameOverride: "shared-postgres" + +image: "postgres:17-alpine" +pullPolicy: IfNotPresent + +auth: + # Bootstrap superuser; owns every per-gear database. + username: platform + password: "platform-demo-pw" + # Maintenance DB the server always creates (used for probes / \gexec host). + maintenanceDb: postgres + +# One database per gear, created idempotently on first init. +databases: + - usersinfo + +persistence: + enabled: false + size: 1Gi + +service: + port: 5432 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 512Mi diff --git a/deploy/helm/toolkit-common/Chart.yaml b/deploy/helm/toolkit-common/Chart.yaml new file mode 100644 index 0000000000..bf84c752b0 --- /dev/null +++ b/deploy/helm/toolkit-common/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v2 +name: toolkit-common +description: >- + CF/Gears ToolKit Helm library chart. Provides reusable named templates + (Deployment, Service, ConfigMap, ServiceAccount, helpers) shared by the + platform-host and per-gear (OoP) charts. Supports Profile 3 (Kubernetes), + including SA-token projection for platform-plane authentication. +type: library +version: 0.1.0 +appVersion: "0.1.0" +keywords: + - cf-gears + - toolkit + - oop + - platform +maintainers: + - name: Constructor Fabric diff --git a/deploy/helm/toolkit-common/templates/_configmap.tpl b/deploy/helm/toolkit-common/templates/_configmap.tpl new file mode 100644 index 0000000000..0acc70a6d4 --- /dev/null +++ b/deploy/helm/toolkit-common/templates/_configmap.tpl @@ -0,0 +1,21 @@ +{{/* +toolkit-common: ConfigMap holding the gear's YAML configuration. + +The config body is supplied verbatim by the consuming chart via +`.Values.config.content` (a multi-line YAML string). It is mounted into the +container at `.Values.config.mountPath` (see the deployment template). Rendered +only when config.content is non-empty. +*/}} +{{- define "toolkit-common.configmap" -}} +{{- if .Values.config.content -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "toolkit-common.fullname" . }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} +data: + {{ .Values.config.fileName }}: | + {{- tpl .Values.config.content . | nindent 4 }} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/toolkit-common/templates/_deployment.tpl b/deploy/helm/toolkit-common/templates/_deployment.tpl new file mode 100644 index 0000000000..dffb83cc75 --- /dev/null +++ b/deploy/helm/toolkit-common/templates/_deployment.tpl @@ -0,0 +1,154 @@ +{{/* +toolkit-common: standard Deployment. + +Supports both the platform-host (host mode, runs the whole in-process stack) and +individual OoP gears (each in its own pod, registering with the platform-host's +DirectoryService). Key Profile 3 wiring: + + * Probes hit the OoP HTTP surface: /healthz (liveness) and /readyz (readiness). + * SA-token projection (values.saToken.enabled) mounts a projected + ServiceAccount token with audience `toolkit-internal` at + /var/run/secrets/tokens/toolkit-internal for platform-plane auth + (TokenReview validation on the receiving gear). + * The gear config ConfigMap is mounted read-only at values.config.mountPath. + * values.directoryEndpoint (OoP gears) is injected as TOOLKIT_DIRECTORY_ENDPOINT. +*/}} +{{- define "toolkit-common.deployment" -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "toolkit-common.fullname" . }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "toolkit-common.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "toolkit-common.selectorLabels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + annotations: + {{- if .Values.config.content }} + checksum/config: {{ include "toolkit-common.configmap" . | sha256sum }} + {{- end }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "toolkit-common.serviceAccountName" . }} + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + {{- with .Values.command }} + command: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.args }} + args: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.containerSecurityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- if .Values.grpc.enabled }} + - name: grpc + containerPort: {{ .Values.grpc.port }} + protocol: TCP + {{- end }} + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- if .Values.directoryEndpoint }} + - name: TOOLKIT_DIRECTORY_ENDPOINT + value: {{ .Values.directoryEndpoint | quote }} + {{- end }} + {{- with .Values.extraEnv }} + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + {{- if .Values.config.content }} + - name: config + mountPath: {{ .Values.config.mountPath }} + subPath: {{ .Values.config.fileName }} + readOnly: true + {{- end }} + {{- if .Values.saToken.enabled }} + - name: toolkit-internal-token + mountPath: {{ .Values.saToken.mountPath }} + readOnly: true + {{- end }} + {{- with .Values.extraVolumeMounts }} + {{- toYaml . | nindent 12 }} + {{- end }} + livenessProbe: + httpGet: + path: {{ .Values.probes.liveness.path }} + port: http + initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.liveness.periodSeconds }} + failureThreshold: {{ .Values.probes.liveness.failureThreshold }} + readinessProbe: + httpGet: + path: {{ .Values.probes.readiness.path }} + port: http + initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }} + periodSeconds: {{ .Values.probes.readiness.periodSeconds }} + failureThreshold: {{ .Values.probes.readiness.failureThreshold }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumes: + {{- if .Values.config.content }} + - name: config + configMap: + name: {{ include "toolkit-common.fullname" . }} + {{- end }} + {{- if .Values.saToken.enabled }} + - name: toolkit-internal-token + projected: + sources: + - serviceAccountToken: + path: {{ .Values.saToken.fileName }} + audience: {{ .Values.saToken.audience }} + expirationSeconds: {{ .Values.saToken.expirationSeconds }} + {{- end }} + {{- with .Values.extraVolumes }} + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end -}} diff --git a/deploy/helm/toolkit-common/templates/_helpers.tpl b/deploy/helm/toolkit-common/templates/_helpers.tpl new file mode 100644 index 0000000000..bc61aa97bb --- /dev/null +++ b/deploy/helm/toolkit-common/templates/_helpers.tpl @@ -0,0 +1,67 @@ +{{/* +toolkit-common: shared name/label helpers. + +These mirror the standard Helm chart conventions so every gear chart produces +consistent metadata, selectors, and service-account names. +*/}} + +{{/* +Chart name (respects nameOverride). +*/}} +{{- define "toolkit-common.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Fully qualified app name (respects fullnameOverride). +*/}} +{{- define "toolkit-common.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Chart label value (name-version). +*/}} +{{- define "toolkit-common.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "toolkit-common.labels" -}} +helm.sh/chart: {{ include "toolkit-common.chart" . }} +{{ include "toolkit-common.selectorLabels" . }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +app.kubernetes.io/part-of: cf-gears-toolkit +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "toolkit-common.selectorLabels" -}} +app.kubernetes.io/name: {{ include "toolkit-common.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Service account name. Uses the created account when serviceAccount.create is +true, otherwise the operator-provided name (defaulting to "default"). +*/}} +{{- define "toolkit-common.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "toolkit-common.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/helm/toolkit-common/templates/_ingress.tpl b/deploy/helm/toolkit-common/templates/_ingress.tpl new file mode 100644 index 0000000000..287ac7ca5d --- /dev/null +++ b/deploy/helm/toolkit-common/templates/_ingress.tpl @@ -0,0 +1,50 @@ +{{/* +toolkit-common: optional Ingress for the HTTP edge. + +Renders only when `.Values.ingress.enabled` is true (default off). Typically +enabled just for the platform-host, which fronts the api-gateway edge: external +traffic enters through the gateway and is reverse-proxied to OoP gears by path +prefix, so gear charts don't define their own Ingress (ADR-0007). + +The backend targets this chart's own Service (`toolkit-common.fullname`) on the +HTTP port (`.Values.service.port`, published as `http`). +*/}} +{{- define "toolkit-common.ingress" -}} +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "toolkit-common.fullname" . }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- $svcName := include "toolkit-common.fullname" . }} + {{- $svcPort := .Values.service.port }} + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ $svcName }} + port: + number: {{ $svcPort }} + {{- end }} + {{- end }} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/toolkit-common/templates/_service.tpl b/deploy/helm/toolkit-common/templates/_service.tpl new file mode 100644 index 0000000000..00743dc700 --- /dev/null +++ b/deploy/helm/toolkit-common/templates/_service.tpl @@ -0,0 +1,34 @@ +{{/* +toolkit-common: ClusterIP (or configurable) Service. + +Exposes the gear's HTTP port (probes + REST/edge). When `.Values.grpc.enabled` +is true, an additional gRPC port is published - used by the platform-host so +OoP gear pods can reach the in-pod grpc-hub / DirectoryService across the +cluster via this Service's DNS name. +*/}} +{{- define "toolkit-common.service" -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "toolkit-common.fullname" . }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + {{- if and (eq .Values.service.type "NodePort") .Values.service.nodePort }} + nodePort: {{ .Values.service.nodePort }} + {{- end }} + {{- if .Values.grpc.enabled }} + - port: {{ .Values.grpc.port }} + targetPort: grpc + protocol: TCP + name: grpc + {{- end }} + selector: + {{- include "toolkit-common.selectorLabels" . | nindent 4 }} +{{- end -}} diff --git a/deploy/helm/toolkit-common/templates/_serviceaccount.tpl b/deploy/helm/toolkit-common/templates/_serviceaccount.tpl new file mode 100644 index 0000000000..7b83e2b75a --- /dev/null +++ b/deploy/helm/toolkit-common/templates/_serviceaccount.tpl @@ -0,0 +1,22 @@ +{{/* +toolkit-common: ServiceAccount. + +Rendered only when serviceAccount.create is true. A dedicated ServiceAccount is +required for SA-token projection (platform-plane auth via TokenReview): the +projected token's identity is this account. +*/}} +{{- define "toolkit-common.serviceAccount" -}} +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "toolkit-common.serviceAccountName" . }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount | default true }} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/toolkit-platform/Chart.yaml b/deploy/helm/toolkit-platform/Chart.yaml new file mode 100644 index 0000000000..893c09b3f7 --- /dev/null +++ b/deploy/helm/toolkit-platform/Chart.yaml @@ -0,0 +1,42 @@ +apiVersion: v2 +name: toolkit-platform +description: >- + CF/Gears ToolKit platform umbrella chart. Deploys the platform-host (edge + + DirectoryService + trust-coupled core + system gears) together with a set of + out-of-process gears. Target: Profile 3 (Kubernetes). +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: platform-host + version: 0.1.0 + repository: "file://../platform-host" + condition: platform-host.enabled + - name: postgres + version: 0.1.0 + repository: "file://../postgres" + condition: postgres.enabled + - name: hello + version: 0.1.0 + repository: "file://../hello" + condition: hello.enabled + - name: users-info + version: 0.1.0 + repository: "file://../users-info" + condition: users-info.enabled + - name: api-contracts + version: 0.1.0 + repository: "file://../api-contracts" + condition: api-contracts.enabled + - name: api-contracts-consumer + version: 0.1.0 + repository: "file://../api-contracts-consumer" + condition: api-contracts-consumer.enabled + - name: cluster + version: 0.1.0 + repository: "file://../cluster" + condition: cluster.enabled + - name: cluster-consumer + version: 0.1.0 + repository: "file://../cluster-consumer" + condition: cluster-consumer.enabled diff --git a/deploy/helm/toolkit-platform/values-dev.yaml b/deploy/helm/toolkit-platform/values-dev.yaml new file mode 100644 index 0000000000..2ed5b1b51b --- /dev/null +++ b/deploy/helm/toolkit-platform/values-dev.yaml @@ -0,0 +1,136 @@ +# Dev preset: platform-host + shared Postgres + 4 OoP gears, locally-built images. +# +# Intended for a minikube demo where images are loaded into the node (so +# pullPolicy: Never avoids a registry round-trip). The Postgres image is public, +# so only platform-host and the 4 gear images need loading: +# +# for img in platform-host hello users-info api-contracts api-contracts-consumer; do +# minikube image load "ghcr.io/constructorfabric/${img}:dev" +# done +# helm install demo deploy/helm/toolkit-platform -f deploy/helm/toolkit-platform/values-dev.yaml + +platform-host: + enabled: true + fullnameOverride: platform-host + image: + repository: ghcr.io/constructorfabric/platform-host + tag: "dev" + pullPolicy: Never + # External entry point via the minikube nginx ingress addon + # (`minikube addons enable ingress`). Reach it without editing /etc/hosts: + # curl --resolve platform-host.local:80:$(minikube ip) \ + # http://platform-host.local/hello/v1/ping + ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-body-size: "64m" + hosts: + - host: platform-host.local + paths: + - path: / + pathType: Prefix + # Connect account-management/resource-group/credstore to the shared Postgres + # below (no local SQLite). + postgres: + host: shared-postgres + user: platform + password: "platform-demo-pw" + databases: + accountManagement: accountmanagement + resourceGroup: resourcegroup + credstore: credstore + +# Shared PostgreSQL (one server, a database per OoP gear). Uses the public +# postgres:17-alpine image, so no local image load is needed. +postgres: + enabled: true + fullnameOverride: shared-postgres + databases: + - accountmanagement + - resourcegroup + - credstore + - usersinfo + - cluster + +hello: + enabled: true + fullnameOverride: hello + image: + repository: ghcr.io/constructorfabric/hello + tag: "dev" + pullPolicy: Never + directoryEndpoint: "http://platform-host:50051" + +users-info: + enabled: true + fullnameOverride: users-info + image: + repository: ghcr.io/constructorfabric/users-info + tag: "dev" + pullPolicy: Never + directoryEndpoint: "http://platform-host:50051" + # Connect to the shared Postgres above (no bundled instance). + postgres: + enabled: false + host: shared-postgres + database: usersinfo + user: platform + password: "platform-demo-pw" + +# OoP gear-to-gear REST demo: the consumer pod resolves the PaymentApi contract +# from the provider pod over REST (via the DirectoryService). +api-contracts: + enabled: true + fullnameOverride: api-contracts + image: + repository: ghcr.io/constructorfabric/api-contracts + tag: "dev" + pullPolicy: Never + directoryEndpoint: "http://platform-host:50051" + +api-contracts-consumer: + enabled: true + fullnameOverride: api-contracts-consumer + image: + repository: ghcr.io/constructorfabric/api-contracts-consumer + tag: "dev" + pullPolicy: Never + directoryEndpoint: "http://platform-host:50051" + +# Cluster coordination demo: the cluster gear (standalone backend) + a consumer +# that resolves ClusterCacheV1 from the cluster pod over gRPC. `fullnameOverride: +# cluster` is REQUIRED — the consumer derives cluster.{namespace}.svc.cluster +# .local:50051, so the Service must be named `cluster`. +cluster: + enabled: true + fullnameOverride: cluster + # Multiple replicas coordinating through the shared Postgres — a single Service + # (:50051) load-balances across them, and shared state keeps reads/writes + # consistent regardless of which pod answers. This is the point of the postgres + # backend (the chart default); standalone would be per-pod and inconsistent. + replicaCount: 3 + image: + repository: ghcr.io/constructorfabric/cluster + tag: "dev" + pullPolicy: Never + directoryEndpoint: "http://platform-host:50051" + # Backing store — the shared Postgres below, database `cluster` (added to + # postgres.databases). Matches the cluster chart's postgres defaults. + postgres: + host: shared-postgres + user: platform + password: "platform-demo-pw" + database: cluster + +cluster-consumer: + enabled: true + fullnameOverride: cluster-consumer + image: + repository: ghcr.io/constructorfabric/cluster-consumer + tag: "dev" + pullPolicy: Never + directoryEndpoint: "http://platform-host:50051" diff --git a/deploy/helm/toolkit-platform/values-minimal.yaml b/deploy/helm/toolkit-platform/values-minimal.yaml new file mode 100644 index 0000000000..c30db52068 --- /dev/null +++ b/deploy/helm/toolkit-platform/values-minimal.yaml @@ -0,0 +1,19 @@ +# Minimal preset: platform-host only (no OoP gears). +# +# helm install platform deploy/helm/toolkit-platform -f deploy/helm/toolkit-platform/values-minimal.yaml + +platform-host: + enabled: true + fullnameOverride: platform-host + +hello: + enabled: false + +users-info: + enabled: false + +api-contracts: + enabled: false + +api-contracts-consumer: + enabled: false diff --git a/deploy/helm/toolkit-platform/values-production.yaml b/deploy/helm/toolkit-platform/values-production.yaml new file mode 100644 index 0000000000..75594940fb --- /dev/null +++ b/deploy/helm/toolkit-platform/values-production.yaml @@ -0,0 +1,53 @@ +# Production preset (scaffold): platform-host + OoP gears from a real registry. +# +# NOTE: this is a starting point for the production stack. It assumes images are +# published to ghcr.io/constructorfabric and pulled normally. Production hardening +# (real database backends, prod plugin presets, SA-token enforcement + RBAC, +# resource tuning, network policies) is tracked in OoP-8 and follow-ups. +# +# helm install platform deploy/helm/toolkit-platform -f deploy/helm/toolkit-platform/values-production.yaml + +platform-host: + enabled: true + fullnameOverride: platform-host + replicaCount: 1 + image: + repository: ghcr.io/constructorfabric/platform-host + tag: "dev" + pullPolicy: IfNotPresent + resources: + requests: + cpu: 500m + memory: 1Gi + limits: + memory: 4Gi + # External entry point in front of the api-gateway edge. Replace the host with + # your real domain and provision a TLS secret (e.g. via cert-manager, then + # reference it under `tls`). + ingress: + enabled: true + className: nginx + annotations: + nginx.ingress.kubernetes.io/proxy-buffering: "off" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-body-size: "64m" + hosts: + - host: platform.example.com + paths: + - path: / + pathType: Prefix + tls: [] + # - hosts: + # - platform.example.com + # secretName: platform-host-tls + +hello: + enabled: true + fullnameOverride: hello + replicaCount: 2 + image: + repository: ghcr.io/constructorfabric/hello + tag: "dev" + pullPolicy: IfNotPresent + directoryEndpoint: "http://platform-host:50051" diff --git a/deploy/helm/toolkit-platform/values.yaml b/deploy/helm/toolkit-platform/values.yaml new file mode 100644 index 0000000000..f3fe5db839 --- /dev/null +++ b/deploy/helm/toolkit-platform/values.yaml @@ -0,0 +1,47 @@ +# Default values for the toolkit-platform umbrella chart. +# +# Subchart values are nested under the subchart name. The platform-host is given +# a stable fullnameOverride ("platform-host") so its Service DNS is predictable +# and every OoP gear can reference it for TOOLKIT_DIRECTORY_ENDPOINT and the edge. + +platform-host: + enabled: true + fullnameOverride: platform-host + image: + repository: ghcr.io/constructorfabric/platform-host + tag: "dev" + pullPolicy: IfNotPresent + +# Shared PostgreSQL: one server, a database per OoP gear. OoP gears connect to +# it (postgres.enabled=false on the gear) instead of each bundling their own. +postgres: + enabled: true + fullnameOverride: shared-postgres + databases: + - usersinfo + +hello: + enabled: true + fullnameOverride: hello + image: + repository: ghcr.io/constructorfabric/hello + tag: "dev" + pullPolicy: IfNotPresent + directoryEndpoint: "http://platform-host:50051" + +users-info: + enabled: true + fullnameOverride: users-info + image: + repository: ghcr.io/constructorfabric/users-info + tag: "dev" + pullPolicy: IfNotPresent + directoryEndpoint: "http://platform-host:50051" + # Use the shared Postgres (above) instead of a bundled one. `host` must match + # the shared server's Service name; `database` must be one of its `databases`. + postgres: + enabled: false + host: shared-postgres + database: usersinfo + user: platform + password: "platform-demo-pw" diff --git a/deploy/helm/users-info/Chart.yaml b/deploy/helm/users-info/Chart.yaml new file mode 100644 index 0000000000..e012443cca --- /dev/null +++ b/deploy/helm/users-info/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: users-info +description: >- + users-info - an authenticated, database-backed REST OoP demo gear. Deploys as + its own pod, authenticates tenant-plane requests locally (embedded authn + stack), resolves the central authz-resolver over REST via the platform host's + DirectoryService for every PEP check, and persists to its own PostgreSQL + database. Reverse-proxied by the api-gateway edge (Profile 3). +type: application +version: 0.1.0 +appVersion: "dev" +dependencies: + - name: toolkit-common + version: 0.1.0 + repository: "file://../toolkit-common" diff --git a/deploy/helm/users-info/templates/configmap.yaml b/deploy/helm/users-info/templates/configmap.yaml new file mode 100644 index 0000000000..dd6093f9d0 --- /dev/null +++ b/deploy/helm/users-info/templates/configmap.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.configmap" . }} diff --git a/deploy/helm/users-info/templates/deployment.yaml b/deploy/helm/users-info/templates/deployment.yaml new file mode 100644 index 0000000000..1101d4a29c --- /dev/null +++ b/deploy/helm/users-info/templates/deployment.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.deployment" . }} diff --git a/deploy/helm/users-info/templates/postgres.yaml b/deploy/helm/users-info/templates/postgres.yaml new file mode 100644 index 0000000000..0ff03ccf45 --- /dev/null +++ b/deploy/helm/users-info/templates/postgres.yaml @@ -0,0 +1,111 @@ +{{- if .Values.postgres.enabled }} +{{- $pg := printf "%s-postgres" (include "toolkit-common.fullname" .) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $pg }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +type: Opaque +stringData: + POSTGRES_DB: {{ .Values.postgres.database | quote }} + POSTGRES_USER: {{ .Values.postgres.user | quote }} + POSTGRES_PASSWORD: {{ .Values.postgres.password | quote }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ $pg }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + type: ClusterIP + selector: + {{- include "toolkit-common.selectorLabels" . | nindent 4 }} + app.kubernetes.io/component: postgres + ports: + - name: postgres + port: 5432 + targetPort: postgres + protocol: TCP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ $pg }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + replicas: 1 + strategy: + type: Recreate + selector: + matchLabels: + {{- include "toolkit-common.selectorLabels" . | nindent 6 }} + app.kubernetes.io/component: postgres + template: + metadata: + labels: + {{- include "toolkit-common.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: postgres + spec: + containers: + - name: postgres + image: {{ .Values.postgres.image | quote }} + imagePullPolicy: {{ .Values.postgres.pullPolicy }} + ports: + - name: postgres + containerPort: 5432 + protocol: TCP + envFrom: + - secretRef: + name: {{ $pg }} + env: + # Keep the default data subdir off the mount root so an emptyDir + # (or PVC) volume mounted at /var/lib/postgresql/data works cleanly. + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + readinessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgres.user | quote }}, "-d", {{ .Values.postgres.database | quote }}] + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 12 + livenessProbe: + exec: + command: ["pg_isready", "-U", {{ .Values.postgres.user | quote }}, "-d", {{ .Values.postgres.database | quote }}] + initialDelaySeconds: 15 + periodSeconds: 15 + failureThreshold: 6 + resources: + {{- toYaml .Values.postgres.resources | nindent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumes: + - name: data + {{- if .Values.postgres.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ $pg }} + {{- else }} + emptyDir: {} + {{- end }} +{{- if .Values.postgres.persistence.enabled }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ $pg }} + labels: + {{- include "toolkit-common.labels" . | nindent 4 }} + app.kubernetes.io/component: postgres +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.postgres.persistence.size | quote }} +{{- end }} +{{- end }} diff --git a/deploy/helm/users-info/templates/service.yaml b/deploy/helm/users-info/templates/service.yaml new file mode 100644 index 0000000000..a27603faad --- /dev/null +++ b/deploy/helm/users-info/templates/service.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.service" . }} diff --git a/deploy/helm/users-info/templates/serviceaccount.yaml b/deploy/helm/users-info/templates/serviceaccount.yaml new file mode 100644 index 0000000000..fbfe1086c3 --- /dev/null +++ b/deploy/helm/users-info/templates/serviceaccount.yaml @@ -0,0 +1 @@ +{{- include "toolkit-common.serviceAccount" . }} diff --git a/deploy/helm/users-info/values.yaml b/deploy/helm/users-info/values.yaml new file mode 100644 index 0000000000..94ec487034 --- /dev/null +++ b/deploy/helm/users-info/values.yaml @@ -0,0 +1,184 @@ +# Default values for the users-info (OoP gear) chart. + +replicaCount: 1 + +image: + repository: ghcr.io/constructorfabric/users-info + tag: "dev" + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + automount: true + annotations: {} + +command: ["/app/gear"] +args: ["--config", "/app/config/gear.yaml"] + +service: + type: ClusterIP + # OoP REST + probes port (matches oop_http.listen_addr). + port: 9092 + +grpc: + enabled: false + port: 50051 + +# DirectoryService (platform-host grpc-hub) this gear registers with and uses to +# resolve the remote authz-resolver. Injected as TOOLKIT_DIRECTORY_ENDPOINT. +directoryEndpoint: "http://platform-host:50051" + +# SA-token projection for platform-plane auth (mounted; used when +# oop_http.internal_auth is configured). +saToken: + enabled: true + audience: toolkit-internal + mountPath: /var/run/secrets/tokens/toolkit-internal + fileName: token + expirationSeconds: 3600 + +probes: + liveness: + path: /healthz + initialDelaySeconds: 3 + periodSeconds: 10 + failureThreshold: 3 + readiness: + path: /readyz + initialDelaySeconds: 3 + periodSeconds: 5 + # Generous: the gear waits for the platform-host DirectoryService + the + # remote authz-resolver dependency to resolve before it reports ready. + failureThreshold: 30 + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 512Mi + +extraEnv: [] +extraVolumes: [] +extraVolumeMounts: [] +podLabels: {} +podAnnotations: {} +podSecurityContext: + runAsNonRoot: true + runAsUser: 1000 +containerSecurityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: ["ALL"] +nodeSelector: {} +affinity: {} +tolerations: [] + +# --------------------------------------------------------------------------- +# PostgreSQL connection. +# +# The gear owns its own logical database. Two topologies are supported via the +# `postgres.enabled` switch: +# +# * enabled: true -> a single-replica Postgres is deployed *alongside* this +# gear (self-contained; `host` defaults to "-postgres"). +# * enabled: false -> the gear connects to an EXISTING/shared Postgres. Set +# `host` to that server's Service DNS (e.g. "shared-postgres") and point +# `database`/`user`/`password` at the gear's database on it. +# +# In the umbrella dev preset we use the shared model: the `postgres` subchart +# deploys one server and this gear connects to its own `usersinfo` database. +# --------------------------------------------------------------------------- +postgres: + enabled: false + # Service DNS of the Postgres server. Empty => "-postgres" (the + # bundled instance). Set to the shared server's name for the shared topology. + host: "shared-postgres" + image: "postgres:17-alpine" + pullPolicy: IfNotPresent + database: usersinfo + user: platform + # Demo credential. Override with `--set postgres.password=...` for anything + # beyond a local minikube demo. Must match the shared server when enabled=false. + password: "platform-demo-pw" + persistence: + enabled: false + size: 1Gi + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + memory: 512Mi + +# --------------------------------------------------------------------------- +# Gear config mounted at /app/config/gear.yaml (rendered with `tpl`, so Helm +# template expressions below are evaluated). +# +# * The gear binds 0.0.0.0:9092 and advertises its in-cluster Service DNS so +# the api-gateway edge (in the platform-host pod) can reverse-proxy to it. +# * PostgreSQL is reached via the bundled Postgres Service (fullname-postgres). +# * The embedded authn stack (authn-resolver + static-authn-plugin) validates +# tenant-plane bearer tokens locally; authz-resolver is resolved remotely. +# --------------------------------------------------------------------------- +config: + mountPath: /app/config/gear.yaml + fileName: gear.yaml + content: | + server: + home_dir: "/app/data" + + logging: + default: + console_format: "json" + console_level: info + + database: + servers: + users_pg: + engine: "postgres" + host: "{{ .Values.postgres.host | default (printf "%s-postgres" (include "toolkit-common.fullname" .)) }}" + port: 5432 + user: {{ .Values.postgres.user | quote }} + password: {{ .Values.postgres.password | quote }} + dbname: {{ .Values.postgres.database | quote }} + pool: + max_conns: 10 + acquire_timeout: "30s" + + oop_http: + listen_addr: "0.0.0.0:9092" + advertise_uri: "http://{{ include "toolkit-common.fullname" . }}:9092" + internal_auth: + provider: kube + audiences: ["toolkit-internal"] + token_path: /var/run/secrets/tokens/toolkit-internal/token + + gears: + users-info: + database: + server: "users_pg" + config: + default_page_size: 50 + max_page_size: 1000 + audit_base_url: "http://audit.local" + notifications_base_url: "http://notifications.local" + + authn-resolver: + config: + vendor: "constructorfabric" + + static-authn-plugin: + config: + vendor: "constructorfabric" + priority: 100 + mode: accept_all + + types-registry: + config: {} diff --git a/deploy/oop-smoke.sh b/deploy/oop-smoke.sh new file mode 100755 index 0000000000..8e5d4ae50b --- /dev/null +++ b/deploy/oop-smoke.sh @@ -0,0 +1,390 @@ +#!/usr/bin/env bash +# +# Profile-3 (Kubernetes) OoP demo, scripted end-to-end. +# +# Codifies the manual walkthrough in deploy/README.md into a repeatable, +# asserting smoke test. By default it only runs the HTTP smoke against an +# already-deployed stack; pass stage flags to also build images, load them into +# minikube, and (re)deploy the umbrella chart. +# +# Usage: +# deploy/oop-smoke.sh # smoke only (assumes stack is deployed) +# deploy/oop-smoke.sh --deploy # helm upgrade + rollout restart + smoke +# deploy/oop-smoke.sh --load --deploy # reload images into minikube, redeploy, smoke +# deploy/oop-smoke.sh --all # build + load + deploy + smoke (full) +# +# Flags (stages run in this fixed order regardless of flag order): +# --build Rebuild all 5 images from the current working tree. +# --load Save + load the 5 images into minikube (tarball; reliable overwrite). +# --deploy helm dependency build/update + upgrade --install + rollout restart. +# --all Shorthand for --build --load --deploy. +# --keep Do not run cleanup on exit (default: nothing is torn down anyway). +# -h|--help Show this help. +# +# Environment overrides: +# NS=cf-gears Kubernetes namespace. +# RELEASE=platform Helm release name. +# IMAGE_TAG=dev Image tag. +# REGISTRY=ghcr.io/constructorfabric +# HOST=platform-host.local +# TOKEN=test-token Bearer token (accept_all maps any non-empty token). +# BUILD_PROFILE=dev +set -euo pipefail + +# ── Resolve repo root (this script lives in deploy/) ─────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SELF="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]}")" # absolute; usage() reads it after cd +ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$ROOT" + +# ── Config ───────────────────────────────────────────────────────────────── +NS="${NS:-cf-gears}" +RELEASE="${RELEASE:-platform}" +IMAGE_TAG="${IMAGE_TAG:-dev}" +REGISTRY="${REGISTRY:-ghcr.io/constructorfabric}" +HOST="${HOST:-platform-host.local}" +# Host port used when the edge is reached via a port-forward fallback (docker +# driver). Matches the api-gateway bind (:8087). +EDGE_PORT="${EDGE_PORT:-8087}" +TOKEN="${TOKEN:-test-token}" +BUILD_PROFILE="${BUILD_PROFILE:-dev}" +TID="00000000-df51-5b42-9538-d2b56b7ee953" +# Max wait for a gear's route to appear at the edge (pod readiness + directory +# registration + edge sync interval). +ROUTE_TIMEOUT="${ROUTE_TIMEOUT:-150}" + +DO_BUILD=0 +DO_LOAD=0 +DO_DEPLOY=0 + +# The images and their generic-per-gear build args (see deploy/README.md §1). +# Format: "||||". +# is the cargo feature list for the OoP build. Most gears need +# "oop_module,k8s-auth"; cluster's OoP bin is unconditional (no oop_module +# feature), so it builds with just "k8s-auth". +GEARS=( + "hello|hello|hello-oop|config/oop-hello.yaml|oop_module,k8s-auth" + "users-info|users-info|users-info-oop|config/oop-users-info.yaml|oop_module,k8s-auth" + "api-contracts|cf-api-contracts|api-contracts-oop|config/oop-api-contracts.yaml|oop_module,k8s-auth" + "api-contracts-consumer|cf-api-contracts-consumer|api-contracts-consumer-oop|config/oop-api-contracts-consumer.yaml|oop_module,k8s-auth" + "cluster|cf-gears-cluster|cluster-oop|config/oop-cluster.yaml|k8s-auth" + "cluster-consumer|cluster-consumer|cluster-consumer-oop|config/oop-cluster-consumer.yaml|oop_module,k8s-auth" +) +ALL_IMAGES=(platform-host hello users-info api-contracts api-contracts-consumer cluster cluster-consumer) + +# ── Pretty output + assertions ───────────────────────────────────────────── +PASS=0 +FAIL=0 +RED=$'\033[31m'; GREEN=$'\033[32m'; YELLOW=$'\033[33m'; BOLD=$'\033[1m'; RESET=$'\033[0m' + +log() { printf '%s\n' "${BOLD}==>${RESET} $*"; } +warn() { printf '%s\n' "${YELLOW}warning:${RESET} $*" >&2; } +die() { printf '%s\n' "${RED}error:${RESET} $*" >&2; exit 1; } + +pass() { PASS=$((PASS+1)); printf ' %sPASS%s %s\n' "$GREEN" "$RESET" "$*"; } +fail() { FAIL=$((FAIL+1)); printf ' %sFAIL%s %s\n' "$RED" "$RESET" "$*"; } + +assert_eq() { #