Skip to content

feat(decofile): add s3 target to serve decofile over HTTP (off etcd) - #33

Merged
hugo-ccabral merged 3 commits into
mainfrom
feat/decofile-s3-target
Jul 23, 2026
Merged

feat(decofile): add s3 target to serve decofile over HTTP (off etcd)#33
hugo-ccabral merged 3 commits into
mainfrom
feat/decofile-s3-target

Conversation

@hugo-ccabral

@hugo-ccabral hugo-ccabral commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Why

Content-heavy sites are hitting the etcd ConfigMap ceiling. deco-sites/econverse has a 5.5 MB decofile — 85% of it inline blog-post HTML (post.content, 345 posts) — which compresses to ~1.02 MB after brotli+base64, over the ~1.00 MB etcd/ConfigMap limit. Once over, the API server rejects the ConfigMap write and the site can no longer publish. Brotli-5 (our only current mitigation) is exhausted.

What

Adds an opt-in spec.target: "s3" to the Decofile CRD. When set, the operator uploads the merged decofile JSON to S3 and points pods at it over HTTP (DECO_RELEASE=https://…) instead of mounting a ConfigMap — removing the etcd ceiling entirely. ConfigMap stays the default; fully reversible (flip target back to configmap).

This alone fixes econverse regardless of blog content. (A follow-up will move blog HTML out of blocks, but it's an optimization once the ceiling is gone.)

Changes

  • CRD (api/v1alpha1/decofile_types.go): s3 in the target enum; Status.ContentHash + Status.S3URL; S3ObjectKey(prefix) + DeploymentIdOrName() helpers (the key derivation shared by reconciler and webhook).
  • Controller (internal/controller/s3sink.go, new): S3Uploader (env-configured, IRSA/Pod-Identity creds) + reconcileS3 — retrieve → content-hash gate (skips unchanged, avoids repo re-download) → PutObject raw JSON → reuse the existing NotifyPodsForDecofile hot-reload → status. Dispatched before the FastDeploy switch; wired in cmd/main.go (nil/inert unless DECOFILE_S3_BUCKET set).
  • Webhook (internal/webhook/v1/service_webhook.go): for target=s3, injectDecofileHTTP sets DECO_RELEASE to the HTTP URL (no ConfigMap volume) and ensureAllowedAuthority merges the host into DECO_ALLOWED_AUTHORITIES, preserving the runtime defaults.
  • Chart: DECOFILE_S3_* env wired via hack/helm-generator/main.go + chart/values.yaml decofileS3: (inert unless bucket set).
  • Tests: S3ObjectKey + mergeAllowedAuthorities.

Key design note

The runtime's HTTP decofile provider (deco/engine/decofile/fetcher.ts) does fetch → JSON.parse with no brotli/base64 decode (that path only exists for file://….bin). So the S3 object is served as plain JSON. S3 has no size limit, so this is fine; gzip + Content-Encoding is a documented deferred optimization (Deno fetch auto-gunzips).

Deploy prerequisites (not in this PR)

  1. S3 bucket co-located with the cluster (private — decofiles can contain secrets), + lifecycle rule for old versions.
  2. Controller AWS access: Pod Identity association / IRSA role with s3:PutObject on the bucket prefix.
  3. Runtime read access: front the private bucket with CloudFront+OAC on an allowed host (e.g. configs.decocdn.com, already in the runtime's default trusted-authority list) and set decofileS3.publicHost.
  4. Set decofileS3.{bucket,region,publicHost} in the operator values (see infra_applications PR).

Test

  • go build ./..., go vet, go test ./api/v1alpha1/, go test -run TestMergeAllowedAuthorities ./internal/webhook/v1/, helm template with decofileS3.bucket set.
  • Pending e2e: create a Decofile with spec.target: s3 on a test site; confirm the operator writes s3://…/decofile.json and no ConfigMap; confirm the pod gets DECO_RELEASE=https://… and boots reading from S3; confirm hot-reload still updates a running pod.

🤖 Generated with Claude Code


Summary by cubic

Adds an opt-in spec.target: "s3" to serve the merged Decofile JSON from S3 over HTTP, bypassing the etcd/ConfigMap size limit while keeping hot-reload. Default stays ConfigMap; enable via Helm decofileS3.* and set spec.target: "s3" to switch.

  • New Features

    • CRD: target: "s3"; status.contentHash and status.s3URL; helpers DeploymentIdOrName() and S3ObjectKey(prefix).
    • Controller: S3Uploader from DECOFILE_S3_*; reconcileS3 does retrieve → SHA-256 gate → PutObject (plain JSON) → pod notify; inert unless DECOFILE_S3_BUCKET is set.
    • Webhook: for target=s3, injects DECO_RELEASE=https://<host>/<key> without a ConfigMap volume; merges the host into DECO_ALLOWED_AUTHORITIES while preserving defaults.
    • Helm/Generator: values.decofileS3.{bucket,region,publicHost[,prefix]} wired to DECOFILE_S3_*.
    • Docs: ARCHITECTURE flows and docs/decofile-s3-onboarding.md.
    • Tests: S3ObjectKey and mergeAllowedAuthorities.
  • Bug Fixes

    • Webhook: removed a redundant PodSpec selector flagged by staticcheck (no behavior change).

Written for commit 3a8430c. Summary will update on new commits.

Review in cubic

Content-heavy sites (e.g. econverse: 5.5MB decofile, 85% inline blog HTML)
compress to ~1.02MB brotli+base64 — over the ~1MB etcd ConfigMap limit, so
publishes start failing. Add an opt-in `spec.target: s3` that uploads the
merged decofile JSON to S3 and points pods at it over HTTP
(DECO_RELEASE=https://…) instead of mounting a ConfigMap, removing the etcd
ceiling entirely.

- CRD: `s3` target enum + Status.ContentHash/S3URL + S3ObjectKey/DeploymentIdOrName helpers
- controller: s3sink.go (S3Uploader + reconcileS3) — retrieve → hash-gate →
  PutObject (raw JSON) → reuse pod hot-reload notifier; inert unless
  DECOFILE_S3_BUCKET is set
- webhook: for target=s3, set DECO_RELEASE to the HTTP URL (no volume mount)
  and merge the host into DECO_ALLOWED_AUTHORITIES preserving runtime defaults
- chart: DECOFILE_S3_* env wired via helm-generator + values.decofileS3
- tests: S3ObjectKey + mergeAllowedAuthorities

Runtime serves plain JSON (the HTTP decofile provider does fetch+JSON.parse
with no brotli/base64 decode). Reversible: flip target back to configmap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nicacioliveira
nicacioliveira previously approved these changes Jul 21, 2026
ARCHITECTURE.md: cold-start and hot-reload sequence diagrams for target=s3
(Flow 7a/7b), a configmap-vs-s3 comparison table, and the design decisions
(no compression, content-hash change detection, shared key derivation,
additive DECO_ALLOWED_AUTHORITIES merge).

docs/decofile-s3-onboarding.md: operational runbook (mirrors
fast-deploy-onboarding.md) — mental model, platform prerequisites, per-site
opt-in steps, verification commands, constraints, troubleshooting, rollback.

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

Copy link
Copy Markdown
Collaborator Author

Added documentation:

  • ARCHITECTURE.md — cold-start/hot-reload sequence diagrams, configmap-vs-s3 comparison, design decisions
  • docs/decofile-s3-onboarding.md — operational runbook (mental model, prerequisites, per-site steps, troubleshooting, rollback)

Infra PR chain for the prerequisites this depends on:

  • decocms/infra_applications#216 (operator chart values)
  • decocms/terraform-foundation-infra#27 (S3 bucket + policies)
  • decocms/terraform-eks-cluster#83 (Pod Identity for the controller)

service.Spec.Template.Spec.PodSpec is an embedded field of Knative's
RevisionSpec, so .Containers is already promoted — .PodSpec.Containers was
flagged as a redundant selector by staticcheck in ensureAllowedAuthority.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hugo-ccabral
hugo-ccabral merged commit 58c893f into main Jul 23, 2026
5 checks passed
@hugo-ccabral
hugo-ccabral deleted the feat/decofile-s3-target branch July 23, 2026 07:12
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.

2 participants