feat/compose-import-example #335

Merged
johnride merged 34 commits from feat/compose-import-example into master 2026-06-19 14:44:25 +00:00

34 Commits

Author SHA1 Message Date
05d63a5532 fix: cargo fmt
All checks were successful
Run Check Script / check (pull_request) Successful in 2m39s
2026-06-19 10:39:31 -04:00
b38205f0bf test(examples): validate the compose-generated chart with real helm lint + template
Some checks failed
Run Check Script / check (pull_request) Failing after 47s
2026-06-18 17:01:32 -04:00
9605892aba feat(examples): deploy a java+react app by importing its docker-compose
Demonstrates the compose-as-source-of-truth path (ADR-026): import an
existing docker-compose.yml into typed Scores, then build/publish/deploy
with the fleet pattern (no ApplicationScore, no ArgoCD).

- compose.rs: docker-compose-types → typed ComposeApp; loud on bind
  mounts / unparseable ports, warns on depends_on/command/restart.
- chart.rs: per-service Deployment+Service (unique filenames), RWX PVC
  per mounted named volume, RollingUpdate/Recreate strategy. Deploy-only
  knobs live here, never in compose or Harmony.toml.
- score.rs: ComposeAppScore = helm upgrade --install + public Ingress.
- publish.rs: docker build/push each built service + helm package/push.
- app/: minimal real Java(SQLite)+React(nginx) compose stack.
- 16 unit tests over import + chart generation.
2026-06-18 17:00:19 -04:00
022e4c130a docs(roadmap): agent secret access full-stack e2e entry
All checks were successful
Run Check Script / check (pull_request) Successful in 2m33s
2026-06-12 14:37:00 -04:00
cb9a649ffa test(fleet-e2e): full-stack device secret access e2e
Two HARMONY_FLEET_VM_E2E-gated tests on a real VM agent, nothing
mocked: (1) agent connects to NATS through the callout with its
Zitadel machine identity, fetches a referenced secret from OpenBao
via the groups claim (flattening Action verified live), and the
container env carries the value (SSH podman inspect ground truth);
(2) deleting the deployment policy denies the next fetch, surfaced
as Phase::Failed naming the denied secret.
2026-06-12 14:37:00 -04:00
99bdc0cb38 feat(fleet-e2e): callout-mode stack — Zitadel + NATS auth callout + OpenBao
AuthMode::Callout now deploys the production auth fabric from the
same Scores fleet_staging_install uses: Zitadel (stable namespace,
groups-claim Action, roles-as-groups) + FleetNatsScore::callout +
NatsAuthCalloutScore + OpenBao with JWT auth bound to the live
issuer. VM devices enroll as Zitadel machine users (username =
device id) and authenticate everywhere with that one identity.

Issuer plumbing: http://sso.fleet-e2e.local:30424 resolves from the
host (k3d port mapping + Host header), in-cluster (CoreDNS rewrite to
a zitadel-e2e NodePort Service whose port equals the nodePort), and
VMs (/etc/hosts → libvirt gateway). 8080 was rejected — dev hosts
commonly occupy it.

Also: Stack::connect_admin for per-test NATS connections (the shared
client's driver dies with the bring-up test's runtime), callout image
joins the generic image builder, harness pins
HARMONY_SECRET_STORE=file + a namespace so credential-persisting
Scores work unconfigured, port-mapping preflight with an actionable
delete-and-recreate error.
2026-06-12 14:37:00 -04:00
ec03cba007 fix(zitadel): pin helm chart to 9.27.1
Newer charts ship a login UI that expects a newer server; with
v4.12.x the login pod crashloops on Token.Invalid.
2026-06-12 14:37:00 -04:00
23e4709f1f feat(fleet): device-side secret injection from OpenBao (ADR-025)
PodmanService gains secret_env (env name + secret name under
<kv>/<prefix>/<deployment>/); the agent resolves references at apply
time through harmony_secret's OpenbaoSecretStore, reusing the NATS
path's ZitadelJwtBearer — one machine identity for both consumers.
Resolution failure surfaces as Phase::Failed naming the denied fetch.

- OpenbaoSecretStore::with_zitadel_bearer: no token-file cache; login
  is lazy and get_raw retries once through refresh_auth on permission
  denied (batch tokens expire without renewal; re-login IS the sync)
- agent [openbao] config section (url + secret_prefix; mounts/role
  default to the staging shape)
- FleetDeviceSetupScore renders the [openbao] block when configured
- resolution binds at apply, never on the periodic tick, keeping the
  secret store off the 30s hot path (fleet-scale login load is the
  ADR-025 constraint)
2026-06-12 14:37:00 -04:00
21149bd66b docs(roadmap): tick groups rollout + branch consolidation as done
All checks were successful
Run Check Script / check (pull_request) Successful in 2m19s
2026-06-12 01:47:38 -04:00
e35614cf22 fix(harmony-k8s): typed apply takes field ownership on SSA conflict
All checks were successful
Run Check Script / check (pull_request) Successful in 2m33s
What harmony applies is declared in code — the source of truth. The
non-forced server-side apply 409'd forever once any other field
manager had touched the resource (e.g. the operator helm chart and
install_crds both installing the fleet CRDs); taking ownership on
conflict is the documented SSA pattern for controllers, and the
dynamic-apply path already supports it.
2026-06-12 01:17:07 -04:00
ef693dff72 fix(fleet-e2e): stage release binaries from CARGO_TARGET_DIR when set
All checks were successful
Run Check Script / check (pull_request) Successful in 2m23s
The harness's child cargo honors CARGO_TARGET_DIR but the staging copy
read <workspace>/target/release unconditionally — image builds fail
with 'No such file or directory' on shared-target setups (e.g. git
worktrees).
2026-06-12 01:01:51 -04:00
3ad2873e44 fix(fleet-e2e): k3d image import --mode direct
All checks were successful
Run Check Script / check (pull_request) Successful in 2m22s
The default tools-node mode can fail to copy the image tar (docker:
'mkdirat var/run: file exists') while still exiting 0 — the image
never lands in containerd and every operator/agent pod ErrImagePulls
at the next bring-up. Direct mode imports through each node's
containerd and actually fails when it fails.
2026-06-12 00:43:09 -04:00
526d2ae3c9 test(fleet-e2e): prove ADR-025 group enforcement against real OpenBao
openbao_groups.rs covers every row of the ADR's convergence table:
member reads / non-member denied, policy attach+detach bind existing
batch tokens at request time, group membership re-reads only at login,
and entity disabled=true kills an in-flight batch token (the ADR-025-3
open item). Self-contained: the JWT mount validates a test RSA key, the
test mints its own device tokens, and drives the real grant backend
over a pod port-forward.

Both openbao tests now run Topology::ensure_ready before raw
Score::interpret — without it the k8s client is never initialized
(stack bring-up did this; the tests didn't).
2026-06-12 00:28:59 -04:00
ca246a49e8 feat(operator): gate scheduling by allowedGroups, sync group grants
Scheduling = members(allowedGroups) ∩ label selector; absent
allowedGroups stays ungated (and secretless). Membership comes from a
DeviceGroupSource snapshot polled every 60s (Zitadel role grants in
prod via ZITADEL_URL/PAT/PROJECT_ID, FLEET_DEVICE_GROUPS static map
for dev/e2e); a membership change re-evaluates only affected devices.

Grant sync flips from device-centric (entity policy lists, O(targeted
devices) writes per deployment) to deployment-centric (allowed groups,
O(1)): synced before the NATS desired-state publish so a device is
never told to run a deployment whose secrets it cannot read. Device
events no longer touch grants at all — grants bind deployments to
groups, not devices.

Operator chart sources the OpenBao/Zitadel env from the operator
Secret, all optional. Test fixtures rename the 'group' label to
'zone': labels are placement vocabulary, the authorization group is a
distinct admin-only concept.
2026-06-12 00:28:59 -04:00
9910b6a414 feat(fleet): allowedGroups on Deployment + device-group sources + groups Action
Groups gate, labels place (ADR-025): DeploymentSpec.allowedGroups
declares which admin-managed device groups may view a deployment —
run it and read its secrets. Labels stay device-reported placement
vocabulary and can never widen access.

DeploymentSecretGrants (deployment → groups, absolute + idempotent)
replaces DeviceSecretAccess; DeviceGroupSource provides membership
(StaticDeviceGroups for dev/e2e, ZitadelDeviceGroups reading project
role grants — roles-as-groups until Zitadel groups GA). The
ZitadelSetupScore groups_claim_action provisions the Action that
flattens role keys into the groups array claim OpenBao binds against;
fleet_staging_install enables it and configures the fleet-device JWT
role (batch tokens, 1h TTL).
2026-06-12 00:28:39 -04:00
ebd65b0d09 feat(openbao): group-grant backend + JWT role groups/batch/static-keys
OpenBaoDeploymentSecretGrants replaces the per-device entity-policy
backend (ADR-025): one policy deployment-<name> attached to each
allowed group's external identity group — O(groups) writes per
deployment change, request-time binding for existing tokens, group +
JWT-mount alias created on first grant so grants precede first login.

OpenbaoJwtAuth grows groups_claim, token_type (fleet roles use batch:
zero storage writes per login, natural expiry is the membership sync)
and jwt_validation_pubkeys so e2e can validate against a static test
key with no live IdP; the PEM is single-quoted for the pod-exec shell.

Chart values disable the agent injector and authDelegator: both unused
and both install cluster-scoped RBAC that outlives namespace cleanup
and breaks the next install.
2026-06-12 00:28:39 -04:00
397f266d31 docs(adr-025): groups are the security boundary — rewrite, trail, guide
Rewrite ADR-025 around the group-based design: groups gate (admin-
managed identity), labels place, OpenBao enforces via external groups;
batch tokens with 1h TTL make expiry re-login the membership sync.
Iteration trail preserved under 025-device-secret-access/ (025-1 JWT
claim scoping, 025-2 entity-by-policy as built, 025-3 the scale and
security analysis with verified sources). Add the user-facing guide
and the implementation + branch-consolidation rollout plan.
2026-06-12 00:26:24 -04:00
659b159251 chore: Never specify Co-Authored-By in commit messages
All checks were successful
Run Check Script / check (pull_request) Successful in 2m33s
2026-06-11 16:36:52 -04:00
6515590bac docs(adr-025): reconcile to as-built entity-by-policy design
All checks were successful
Run Check Script / check (pull_request) Successful in 2m22s
The implementation superseded ADR-025's JWT-`deployments`-claim /
`groups_claim` scoping with OpenBao entity-by-policy (device keeps one
token; membership changes rewrite the entity's policy list — no re-auth).
Mark the ADR Accepted-revised with an Addendum recording the pivot and
what was consequently dropped (groups_claim, Zitadel Action, cached_scope,
agent refresh-check). Flag the handoff work-plan as superseded with an
as-built mapping. Coordinate final ADR wording with the original author.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:50:48 -04:00
04f210fc33 fix(examples/openbao): drop removed entities field from OpenbaoSetupScore
The entity concept moved to identity.rs/OpenBaoDeviceSecretAccess; the
example still set the now-removed `entities` field (E0560).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:50:48 -04:00
e2c5dcfab4 feat(harmony_config): wire OpenBao JWT-bearer rung from env
openbao_from_env() hardcoded zitadel_jwt_bearer/jwt_role/jwt_auth_mount to
None, so a generic ConfigClient consumer could never use the headless
Zitadel-machine → OpenBao login rung. Build a ZitadelJwtBearerConfig from
env when a machine keyfile (HARMONY_ZITADEL_KEY_PATH or _KEY_JSON) plus
HARMONY_ZITADEL_AUDIENCE are present; the issuer reuses the existing
HARMONY_SSO_URL (one source of truth for the Zitadel base). Also wire
OPENBAO_JWT_ROLE / OPENBAO_JWT_AUTH_MOUNT so the rung can complete
/auth/jwt/login. Absent any piece, the rung stays off and the auth ladder
falls through unchanged.

Re-export ZitadelJwtBearerConfig from harmony_secret so consumers can build
the options.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:50:48 -04:00
62471e2edf fix: break harmony_secret↔harmony_zitadel_auth dependency cycle
Consolidating the stack exposed a cycle the individual PRs didn't have:
master made harmony_zitadel_auth high-level (config.rs derives
harmony_config::Config → depends on harmony_config), while the JWT-bearer
branch made harmony_secret depend on harmony_zitadel_auth for the token
minter. Result: harmony_secret → harmony_zitadel_auth → harmony_config →
harmony_secret. Cargo refuses to build it.

The minter (jwt_bearer) is a self-contained, harmony-free leaf used by
harmony_secret + harmony-fleet-auth (and re-exported by
harmony_zitadel_auth). Move it into a new leaf crate harmony_zitadel_jwt;
harmony_zitadel_auth re-exports MachineKeyFile/ZitadelJwtBearer from it so
downstream consumers are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:50:48 -04:00
4ff7dcc9dd Merge fleet device-secret-access (JWT-bearer OpenBao auth) into master
Consolidates the #319 (docs/ADR-025) + #316 (auth next-url redirect) +
#332 (JWT-bearer OpenBao auth, entity-by-policy device secret access)
stack onto current master.

Conflict resolutions:
- harmony_zitadel_auth config: keep master's typed ConfigClient approach
  (OperatorCookieKey + ZitadelAuthConfig via Config derive); drop the
  branch's superseded config_from_env/cookie_key_from_env/*_ENV path.
  Operator already loads via ConfigClient on master.
- harmony_zitadel_auth/lib.rs: master's config exports + branch's
  jwt_bearer exports (MachineKeyFile, ZitadelJwtBearer).
- fleet-auth/lib.rs: keep master's OPERATOR_CREDENTIALS_ENV_VAR export.
- operator/main.rs: keep master's in-process spawn_dashboard + branch's
  openbao secret-access wiring and extended run() signature; drop the
  removed device_status task.
- dev.sh: master's HARMONY_CONFIG_* typed env vars.

#316's next-url-redirect feature (login.rs valid_next/RawAuthCallbackQuery)
is preserved unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:21:20 -04:00
d453d2c6be add new files
Some checks failed
Run Check Script / check (pull_request) Failing after 53s
2026-06-07 12:51:44 -04:00
649cfe84bf resolve conflict, add files ob entity policy 2026-06-05 13:08:44 -04:00
8d7ffeee91 Merge branch 'feat/auth-add-next-url-redirect' into feat/jwt-bearer-openbao-auth 2026-06-04 11:00:15 -04:00
57d9685c57 add files
All checks were successful
Run Check Script / check (pull_request) Successful in 2m17s
2026-06-02 12:46:42 -04:00
788227b8c0 stand alone module for jwt
Some checks failed
Run Check Script / check (pull_request) Failing after 50s
2026-06-02 10:32:11 -04:00
3d01d7482f docs: Simplify architecture for openbao sso via harmony config
All checks were successful
Run Check Script / check (pull_request) Successful in 2m16s
2026-06-01 15:15:42 -04:00
aecb08a7e9 docs: fix openbao sso implementation plan to reuse harmony_config instead of writing a new implementation of a secret store
All checks were successful
Run Check Script / check (pull_request) Successful in 2m19s
2026-06-01 15:08:01 -04:00
c71fde4344 docs: ADR on how to handle securely fleet device secrets with openbao + zitadel SSO
All checks were successful
Run Check Script / check (pull_request) Successful in 2m28s
2026-06-01 14:59:07 -04:00
e220f2ff10 remove env file
Some checks failed
Run Check Script / check (pull_request) Failing after 51s
2026-05-31 15:09:25 -04:00
a185ccc78a update env variable names 2026-05-31 15:08:06 -04:00
aacbc509b1 add files for feature
Some checks failed
Run Check Script / check (pull_request) Failing after 52s
2026-05-31 14:59:00 -04:00