Skip to content

feat(app-service): materialize manifest secrets[] as kubernetes secrets - #3758

Draft
RonnyPfannschmidt wants to merge 2 commits into
beclab:mainfrom
RonnyPfannschmidt:feat/manifest-secrets-block
Draft

feat(app-service): materialize manifest secrets[] as kubernetes secrets#3758
RonnyPfannschmidt wants to merge 2 commits into
beclab:mainfrom
RonnyPfannschmidt:feat/manifest-secrets-block

Conversation

@RonnyPfannschmidt

@RonnyPfannschmidt RonnyPfannschmidt commented Jul 24, 2026

Copy link
Copy Markdown

Repo: beclab/Olares · Branch: feat/manifest-secrets-block · Lands: SECOND

Important

Blocked on beclab/api#5 — "feat(manifest): add top-level secrets[] block to OlaresManifest".
This branch does not compile in CI until that merges and the one-line go.mod bump below is
applied. See Before merging.

What

Implements the top-level secrets: block of OlaresManifest.yaml:

secrets:
  - name: smtp-password
    valueFrom:
      envName: OLARES_USER_SMTP_PASSWORD
    applyOnChange: true

Each declaration becomes its own Opaque Secret in the app namespace, named verbatim, holding the
resolved value under the constant key value. The chart references it with nothing templated:

env:
  - name: SMTP_PASSWORD
    valueFrom:
      secretKeyRef:
        name: smtp-password
        key: value

Why

The existing envs[].valueFrom path resolves a SystemEnv/UserEnv and renders it into
.Values.olaresEnv.<NAME>, which charts drop into the container env: as a literal value — so
it is visible to anyone who can read the Deployment or its ReplicaSet. secrets[] keeps such values
in a Secret instead, letting authors write meaningfully safer charts.

This mirrors the model middleware credentials already use (tapr renders secretKeyRef against a
provisioned Secret) rather than inventing a new one.

Change propagation

Rotating a referenced SystemEnv/UserEnv reaches secret-backed apps through the same chain that
already propagates env changes
— not a parallel mechanism:

SystemEnv/UserEnv change
  → SystemEnv/UserEnv controller: appEnvReferences(appEnv)   ← now scans Secrets, not just Envs
  → annotate AppEnv → AppEnvController.syncSecretValues
  → ApplySecretsFor rewrites the Secret, reports what changed
  → if the declaration set applyOnChange → NeedApply → ApplyEnvOp → helm upgrade → pods restart

Recording the declarations on the AppEnv CR (the api PR) is what makes the affected apps
discoverable; appEnvReferences — shared by both env controllers so they cannot drift — now matches
secrets[] as well as envs[]. This inherits the operational guards already proven on that path:
deferral while an app is Stopped, the env-batch-lock lease, and re-enqueue on resume.

Design notes

  • No new CRD. The Kubernetes Secret is the storage.
  • The resolved value is never written onto the AppEnv CR — only the declaration. AppSecretVar
    has no field to hold a value. Change detection compares against the existing Secret, which already
    holds the previous value, so nothing sensitive (nor a hash of it) is persisted for that purpose.
    A test serializes the CR and fails if the value ever appears in it.
  • applyOnChange gates the redeploy. Secret values are injected into pods at start time, so
    without a redeploy the Secret is refreshed while running pods keep serving the old value. A changed
    secret without applyOnChange refreshes the Secret silently and takes effect on the next restart.
  • No spurious redeploys. Re-applying identical data reports no change, so a no-op reconcile
    cannot trigger a helm upgrade.
  • Precedence follows envs[]: the owner's UserEnv overrides a same-named cluster SystemEnv.
  • Fails closed on a bad declaration (unknown env reference, missing valueFrom.envName, empty
    name, duplicate names) rather than silently producing an empty Secret.
  • Cleanup is inherited: uninstall deletes the app namespace (helm_ops_uninstall.go:79), which
    removes the Secrets. No teardown code added.

TODO(structured-secrets) marks the single-value-per-Secret shape; multi-key secrets are deferred
until the Olares UX can author them.

Changes

File Change
pkg/appcfg/types.go AppSecretVar alias + re-exported AppSecretValueKey
pkg/appcfg/application.go Secrets on ApplicationConfig (round-trips through the JSON Spec.Config)
pkg/utils/app/app.go record secrets[] on the AppEnv CR in ApplyAppEnv; change-aware ApplySecretsFor returning the names that changed; shared validateAppSecretDecls
controllers/appenv_controller.go syncSecretValues (→ NeedApply on real change when applyOnChange); shared appEnvReferences helper
controllers/systemenv_controller.go, controllers/userenv_controller.go isReferenced delegates to appEnvReferences, which now scans Secrets
pkg/utils/app/secrets_test.go, controllers/appenv_secrets_test.go new tests
.gitignore ignore .deps/ + go.work.sum for the cross-repo workflow (see below)

Tests

  • schema block decodes into the config (guards a silent-drop regression: the loader is non-strict, so
    a missing struct tag would fail closed with no secret at all)
  • one Secret per declaration; correct name/key/value/type
  • ApplySecretsFor reports only real changes (create → change; identical re-apply → none; rotate one
    of two → just that one) — the contract the redeploy decision rests on
  • declarations are recorded on the AppEnv CR; the resolved value is not (serialize-and-scan)
  • the pending status write does not mutate the caller's ApplicationConfig
  • appEnvReferences matches via env, via secret, and mixed; ignores a declaration without valueFrom
  • structural guard: AppSecretVar has no value-bearing field
  • UserEnv overrides SystemEnv; four failure modes; no-op when no secrets[]

Before merging

  1. Merge feat(manifest): add top-level secrets[] block to OlaresManifest api#5.
  2. Bump the pin in framework/app-service/go.mod:
    cd framework/app-service && go get github.com/beclab/api@<merge-sha>
  3. Re-run: go build ./framework/app-service/... and
    go test ./framework/app-service/pkg/appcfg/... ./framework/app-service/pkg/utils/app/... ./framework/app-service/controllers/...
  4. gh pr ready this PR.

The bump is intentionally not in this branch: the target pseudo-version does not exist yet, so
committing a placeholder would only produce a misleading diff.

Verification so far

Verified locally with a gitignored go.work pointing at a local beclab/api checkout of the api PR:

  • go build ./framework/app-service/... passes; vet clean on the touched packages
  • appcfg, utils/app, controllers suites pass, including the new tests
  • framework/oac builds and its full suite passes against the same schema

Two failures in the wider app-service suite — TestMatchVersion and a 600s hang in pkg/task — are
pre-existing, confirmed by stashing this branch's changes and reproducing them at main.

On this machine the build needs -tags exclude_graphdriver_btrfs; containers/storage wants btrfs
headers. Unrelated to this change.

Not included (follow-ups)

  • oac validation for secrets[]. oac parses non-strictly, so a typo'd entry currently passes
    chart lint silently and only surfaces at install. Natural rules: non-empty name, valid
    DNS-1123 subdomain, valueFrom.envName required, no duplicates. Own PR (also needs oac's v0.0.17
    pin bumped); benefits cli immediately via its local replace.
  • Docs for chart authors (docs/developer/develop/package/manifest.md + the zh counterpart),
    and a pointer from the existing type: password env example toward secrets[].

Note on the .gitignore hunk

Adds .deps/ and go.work.sum so a local checkout of github.com/beclab/api can be wired in via a
(gitignored) go.work while developing across both repos. Unrelated to the runtime change and easy
to drop if you'd rather not carry it.


🤖 Generated with Claude Code

Implements the top-level `secrets:` block of OlaresManifest.yaml. Each
entry pulls one Olares-provided env var (a SystemEnv or UserEnv,
referenced exactly like envs[].valueFrom) into its own Opaque Secret in
the app namespace, named verbatim after the entry and holding the value
under the constant key `value`.

The chart then consumes it with a plain secretKeyRef:

    env:
      - name: SMTP_PASSWORD
        valueFrom:
          secretKeyRef:
            name: smtp-password
            key: value

This closes a gap in the existing envs[] path: values pulled via
olaresEnv are rendered into helm values and end up as literal env values
in the pod spec, visible to anyone who can read the Deployment or its
ReplicaSet. secrets[] keeps them in a Secret instead. It mirrors the
model middleware credentials already use (tapr renders secretKeyRef
against a provisioned Secret) rather than inventing a new one.

Deliberately no new CRD: the Kubernetes Secret is the storage. Values
are re-resolved and rewritten on every install and upgrade so a changed
SystemEnv/UserEnv propagates. Cleanup is inherited from namespace
deletion on uninstall.

ApplySecrets runs from the existing applyAppEnv step on both the install
and upgrade paths, ahead of the helm install, so the Secrets exist by
the time pods start. The V3 helpers embed these, so all paths are
covered.

This is a transition helper built from existing primitives, expected to
be superseded once Olares provides first-class secrets; the
single-value-per-Secret shape is marked TODO(structured-secrets).

REQUIRES the schema change in beclab/api (top-level Secrets field on
AppConfiguration). The go.mod pin bump is intentionally NOT part of this
commit because the target pseudo-version does not exist until that lands
first; see the PR description.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

@RonnyPfannschmidt is attempting to deploy a commit to the Lucky's projects Team on Vercel.

A member of the Team first needs to authorize it.

Hooks secrets[] into the existing env change-propagation chain instead of
only materializing Secrets at install and upgrade time.

Previously a rotated SystemEnv/UserEnv reached apps consuming it via
envs[], but silently never reached apps consuming it as a secret: the
SystemEnv/UserEnv controllers discover affected apps by scanning
appEnv.Envs, and secret declarations were deliberately not persisted
anywhere ("the Secret is the storage"). An app kept serving a stale
credential until someone happened to reinstall it — the worst failure
mode for a secrets feature, and precisely the case rotation exists for.

The declarations are now recorded on the AppEnv CR alongside Envs, which
makes them discoverable, and the rest of the chain is reused rather than
reinvented: annotate -> sync -> NeedApply -> ApplyEnvOp -> helm upgrade.
That also inherits the operational guards already proven there (deferral
while an app is Stopped, the env batch lease, re-enqueue on resume).

Only the declaration is stored, never the resolved value; AppSecretVar has
no field able to hold one. Change detection compares against the existing
Secret, which already holds the previous value, so nothing sensitive (nor
a hash of it) needs persisting to detect a rotation. A no-op reconcile
rewrites identical data, reports no change, and cannot trigger a spurious
redeploy.

A changed secret only requests a redeploy when the declaration sets
applyOnChange. This matters more than for envs: secret values are injected
into pods at start time, so without the redeploy the Secret is refreshed
while running pods keep serving the old value.

isReferenced in the SystemEnv and UserEnv controllers was duplicated
verbatim; both now delegate to one appEnvReferences helper so the two
cannot drift on which reference kinds they recognize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant