From 5484ed7db6d4f0ecf0621825b9f0f0897b44fe08 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 6 May 2026 17:33:32 -0500 Subject: [PATCH] Adopt fc-updater into ArgoCD --- apps/fc-updater/README.md | 47 ++++ apps/fc-updater/fc-updater.yaml | 248 ++++++++++++++++++ apps/fc-updater/kustomization.yaml | 7 + apps/flowercore/flowercore.yaml | 48 +--- .../FleetManifestLintTests.cs | 6 + .../08_public_readwrite_allowlist.rego | 7 +- 6 files changed, 328 insertions(+), 35 deletions(-) create mode 100644 apps/fc-updater/README.md create mode 100644 apps/fc-updater/fc-updater.yaml create mode 100644 apps/fc-updater/kustomization.yaml diff --git a/apps/fc-updater/README.md b/apps/fc-updater/README.md new file mode 100644 index 0000000..6228059 --- /dev/null +++ b/apps/fc-updater/README.md @@ -0,0 +1,47 @@ +# fc-updater — Update Center GitOps adoption + +**Status:** adopted into `bluejay-infra` on 2026-05-06. The live ArgoCD +Application is `infra-fc-updater`, generated by the `bluejay-infra` +ApplicationSet with automated sync, `prune: true`, and `selfHeal: true`. + +## Managed manifest set + +`apps/fc-updater/fc-updater.yaml` manages: + +- `Namespace/fc-updater` +- `PersistentVolumeClaim/updatecenter-data` +- `Deployment/updatecenter-web` +- `Service/updatecenter-web` +- `Certificate/updatecenter-web-tls` +- `Certificate/updatecenter-web-internal-tls` +- `IngressRoute/updatecenter-web` +- `IngressRoute/updatecenter-web-internal` +- `IngressRoute/updatecenter-web-public` + +The Deployment intentionally sets `revisionHistoryLimit: 3` and +`strategy.type: Recreate`. The service is singleton + SQLite/local bundle +storage on `PersistentVolumeClaim/updatecenter-data`, pinned to +`rke2-server`. + +## Runtime dependencies intentionally not stored here + +These live Secrets are pre-existing runtime material and are not committed to +Git: + +- `updater-bootstrap-auth` +- `updater-signing` +- `updater-webhooks` +- `cf-origin-flowercore-io` + +Rotate the Cloudflare Origin Certificate through +`FlowerCore.Notes/docs/standards/code-signing-rotation-runbook.md`; the +shared origin cert must exist in every namespace that serves a +`*.flowercore.io` public IngressRoute. + +## Verification + +```powershell +kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n argocd get application infra-fc-updater +kubectl.exe --kubeconfig C:\Users\AndrewStoltz\.kube\rke2.yaml -n fc-updater get deploy,svc,ingressroute,certificate,pvc +curl.exe -sk https://update.flowercore.io/api/v1/manifests/_schema +``` diff --git a/apps/fc-updater/fc-updater.yaml b/apps/fc-updater/fc-updater.yaml new file mode 100644 index 0000000..fd0cf94 --- /dev/null +++ b/apps/fc-updater/fc-updater.yaml @@ -0,0 +1,248 @@ +# FlowerCore Update Center +# GitOps adoption of the live fc-updater namespace after PUB-1/PUB-3. +# Runtime credentials remain in existing K8s Secrets; do not store them here. +--- +apiVersion: v1 +kind: Namespace +metadata: + name: fc-updater + labels: + app.kubernetes.io/part-of: flowercore +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: updatecenter-data + namespace: fc-updater + labels: + app.kubernetes.io/name: updatecenter-web + app.kubernetes.io/part-of: flowercore +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + volumeMode: Filesystem + resources: + requests: + storage: 10Gi +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: updatecenter-web + namespace: fc-updater + labels: + app: updatecenter-web + app.kubernetes.io/name: updatecenter-web + app.kubernetes.io/part-of: flowercore +spec: + replicas: 1 + revisionHistoryLimit: 3 + strategy: + # SQLite + local bundle storage live on a single RWO PVC. Recreate avoids + # two pods overlapping the same write path during future image bumps. + type: Recreate + selector: + matchLabels: + app: updatecenter-web + template: + metadata: + labels: + app: updatecenter-web + spec: + nodeName: rke2-server + containers: + - name: web + image: localhost/fc-updater-web:v20260506-pub3-fix1 + imagePullPolicy: Never + ports: + - containerPort: 8080 + name: http + env: + - name: ASPNETCORE_URLS + value: http://+:8080 + - name: FlowerCore__Updater__Database__Provider + value: sqlite + - name: FlowerCore__Updater__Database__ConnectionString + value: Data Source=/data/updatecenter.db + - name: FlowerCore__Updater__BundleStorage__LocalFs__RootDirectory + value: /data/bundles + - name: FlowerCore__Updater__Auth__Bootstrap__Enabled + value: "true" + - name: FlowerCore__Updater__Auth__Bootstrap__Username + valueFrom: + secretKeyRef: + name: updater-bootstrap-auth + key: username + - name: FlowerCore__Updater__Auth__Bootstrap__Password + valueFrom: + secretKeyRef: + name: updater-bootstrap-auth + key: password + - name: FlowerCore__Updater__Auth__Bootstrap__SigningKey + valueFrom: + secretKeyRef: + name: updater-bootstrap-auth + key: signing-key + - name: FlowerCore__Updater__Signing__AutoSignOnPublish + value: "true" + - name: FlowerCore__Updater__Signing__RequireSignatureOnPublish + value: "true" + - name: FlowerCore__Updater__Signing__PfxBase64 + valueFrom: + secretKeyRef: + name: updater-signing + key: pfx-base64 + - name: FlowerCore__Updater__Signing__PfxPassword + valueFrom: + secretKeyRef: + name: updater-signing + key: pfx-password + - name: FlowerCore__Updater__Signing__OpItemReference + value: op://FlowerCore/step-ca-codesign + - name: FlowerCore__Updater__Signing__TrustAnchorPath + value: /etc/flowercore-updater/signing/root-ca.pem + - name: FlowerCore__Updater__GitHub__Token + valueFrom: + secretKeyRef: + name: updater-webhooks + key: github-token + - name: FlowerCore__Updater__GitHub__WebhookSecret + valueFrom: + secretKeyRef: + name: updater-webhooks + key: github-webhook-secret + - name: FlowerCore__Updater__Gitea__Token + valueFrom: + secretKeyRef: + name: updater-webhooks + key: gitea-token + - name: FlowerCore__Updater__Gitea__WebhookSecret + valueFrom: + secretKeyRef: + name: updater-webhooks + key: gitea-webhook-secret + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 10 + periodSeconds: 15 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + volumeMounts: + - name: data + mountPath: /data + - name: signing + mountPath: /etc/flowercore-updater/signing + readOnly: true + volumes: + - name: data + persistentVolumeClaim: + claimName: updatecenter-data + - name: signing + secret: + secretName: updater-signing + items: + - key: root-ca.pem + path: root-ca.pem +--- +apiVersion: v1 +kind: Service +metadata: + name: updatecenter-web + namespace: fc-updater + labels: + app: updatecenter-web + app.kubernetes.io/name: updatecenter-web + app.kubernetes.io/part-of: flowercore +spec: + type: ClusterIP + selector: + app: updatecenter-web + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: updatecenter-web-tls + namespace: fc-updater +spec: + secretName: updatecenter-web-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - updatecenter.iamworkin.lan + - updates.iamworkin.lan +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: updatecenter-web-internal-tls + namespace: fc-updater +spec: + secretName: updatecenter-web-internal-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - updatecenter-internal.iamworkin.lan +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: updatecenter-web + namespace: fc-updater +spec: + entryPoints: + - web + - websecure + routes: + - match: (Host(`updatecenter.iamworkin.lan`) || Host(`updates.iamworkin.lan`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`)) + kind: Rule + services: + - name: updatecenter-web + port: 8080 + tls: + secretName: updatecenter-web-tls +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: updatecenter-web-internal + namespace: fc-updater +spec: + entryPoints: + - web + - websecure + routes: + - match: Host(`updatecenter-internal.iamworkin.lan`) + kind: Rule + services: + - name: updatecenter-web + port: 8080 + tls: + secretName: updatecenter-web-internal-tls +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: updatecenter-web-public + namespace: fc-updater +spec: + entryPoints: + - websecure + routes: + - match: (Host(`update.flowercore.io`) || Host(`updates.flowercore.io`)) && (Method(`GET`) || Method(`HEAD`) || Method(`POST`) || Method(`OPTIONS`)) + kind: Rule + services: + - name: updatecenter-web + port: 8080 + tls: + secretName: cf-origin-flowercore-io diff --git a/apps/fc-updater/kustomization.yaml b/apps/fc-updater/kustomization.yaml new file mode 100644 index 0000000..f954431 --- /dev/null +++ b/apps/fc-updater/kustomization.yaml @@ -0,0 +1,7 @@ +# ArgoCD's bluejay-infra ApplicationSet uses a directory generator and does +# not require kustomization.yaml. Keep this anyway as the manifest inventory +# and for local `kubectl kustomize apps/fc-updater` previews. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - fc-updater.yaml diff --git a/apps/flowercore/flowercore.yaml b/apps/flowercore/flowercore.yaml index cf2ad40..b5b1b14 100644 --- a/apps/flowercore/flowercore.yaml +++ b/apps/flowercore/flowercore.yaml @@ -1,6 +1,11 @@ -# FlowerCore Tenant — flowercore.io (main brand) -# Public-facing placeholder landing page served by nginx -# ArgoCD managed - BlueJay Lab +# FlowerCore Tenant — retired flowercore.io placeholder. +# +# Public flowercore.io/www.flowercore.io routing is now owned by +# apps/fc-landing/fc-landing.yaml. This tenant placeholder remains available +# only as an in-cluster service; do not create a duplicate public +# IngressRoute here because it competes with fc-landing and requires a +# namespace-local cf-origin-flowercore-io Secret. +# ArgoCD managed - BlueJay Lab --- apiVersion: v1 kind: Namespace @@ -10,15 +15,9 @@ metadata: app.kubernetes.io/part-of: bluejay-infra flowercore.io/tenant: flowercore --- -# NOTE: The existing cf-origin-flowercore-io secret (covering *.flowercore.io) -# must be copied into this namespace. It already exists in other namespaces. -# Copy with: kubectl get secret cf-origin-flowercore-io -n fc-system -o yaml \ -# | sed 's/namespace: .*/namespace: tenant-flowercore/' \ -# | kubectl apply -f - ---- -# Landing page HTML -apiVersion: v1 -kind: ConfigMap +# Landing page HTML +apiVersion: v1 +kind: ConfigMap metadata: name: flowercore-web-html namespace: tenant-flowercore @@ -308,25 +307,6 @@ spec: selector: app: flowercore-web ports: - - port: 80 - targetPort: 80 - name: http ---- -# Traefik IngressRoute — public via Cloudflare -# Uses existing cf-origin-flowercore-io cert (must be copied to this namespace) -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: flowercore-web - namespace: tenant-flowercore -spec: - entryPoints: - - websecure - routes: - - match: Host(`flowercore.io`) || Host(`www.flowercore.io`) - kind: Rule - services: - - name: flowercore-web - port: 80 - tls: - secretName: cf-origin-flowercore-io + - port: 80 + targetPort: 80 + name: http diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index 9f14017..4bba10f 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -22,10 +22,16 @@ public sealed class FleetManifestLintTests // (bootstrap-JWT) so its allowlist is GET||HEAD||POST||OPTIONS — but // PUT/PATCH/DELETE must still 404 at the route. Anything wider than this // set should fail this lint. + // + // PUB-1 (2026-05-06): update.flowercore.io / updates.flowercore.io were + // added for the Cloudflare-proxied public Update Center edge. They use the + // same bounded read-write allowlist as the LAN pair. private static readonly HashSet PublicReadWriteAllowlistHosts = new(StringComparer.Ordinal) { "updatecenter.iamworkin.lan", "updates.iamworkin.lan", + "update.flowercore.io", + "updates.flowercore.io", }; private static readonly HashSet ApiKeyProtectedDeployments = new(StringComparer.Ordinal) diff --git a/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego b/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego index ebb1152..00a701c 100644 --- a/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego +++ b/tests/bluejay-infra-lint/conftest.dev/08_public_readwrite_allowlist.rego @@ -6,7 +6,12 @@ package bluejayinfra.public_readwrite_allowlist # PUT/PATCH/DELETE must still 404 at the route. Any host in this set MUST # include all four required methods AND MUST NOT include any forbidden # method. -public_readwrite_hosts := {"updatecenter.iamworkin.lan", "updates.iamworkin.lan"} +public_readwrite_hosts := { + "updatecenter.iamworkin.lan", + "updates.iamworkin.lan", + "update.flowercore.io", + "updates.flowercore.io", +} required_methods := {"GET", "HEAD", "POST", "OPTIONS"}