From 4c949a8278d66cd893d9c18cc994d077bdf9a829 Mon Sep 17 00:00:00 2001 From: Andrew Stoltz Date: Sun, 17 May 2026 23:27:12 -0500 Subject: [PATCH] feat(brochure): add public brochure GitOps app --- apps/brochure/README.md | 27 ++++ apps/brochure/brochure.yaml | 131 ++++++++++++++++++ .../FleetManifestLintTests.cs | 1 + .../02_public_method_allowlist.rego | 2 +- 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 apps/brochure/README.md create mode 100644 apps/brochure/brochure.yaml diff --git a/apps/brochure/README.md b/apps/brochure/README.md new file mode 100644 index 0000000..8f4c3bc --- /dev/null +++ b/apps/brochure/README.md @@ -0,0 +1,27 @@ +# FlowerCore Brochure + +`apps/brochure` hosts the public brochure split from `FlowerCore.Intranet.Web`. +ArgoCD's `apps/*` ApplicationSet will create `infra-brochure` after this +directory lands on `main`. + +## Runtime + +- Host: `https://brochure.flowercore.io` +- Namespace: `brochure` +- Deployment: `brochure-web` +- Image: `localhost/fc-brochure-web:v20260524-sprint32` +- Port: `8080` +- Public route method allowlist: `GET` and `HEAD` + +## Operator Actions + +1. Publish and import `localhost/fc-brochure-web:v20260524-sprint32` to every + RKE2 node before sync, using the same podman save + `ctr images import` + flow as the Intranet deployment. +2. Create the Cloudflare DNS record for `brochure.flowercore.io` pointing at + the FlowerCore public edge. +3. Verify `infra-brochure` appears in ArgoCD, the certificate becomes Ready, + and `GET https://brochure.flowercore.io/` returns `200`. + +The route intentionally does not expose `/ops/*` or `/admin/*`; the Brochure +web app returns `404` for those paths and Traefik only forwards read methods. diff --git a/apps/brochure/brochure.yaml b/apps/brochure/brochure.yaml new file mode 100644 index 0000000..ccdbada --- /dev/null +++ b/apps/brochure/brochure.yaml @@ -0,0 +1,131 @@ +# FlowerCore Brochure public host +# +# Thin Blazor host for public What's New, walkthrough, and gallery content +# carved out of FlowerCore.Intranet.Web. The ApplicationSet creates +# infra-brochure from this directory after merge. +--- +apiVersion: v1 +kind: Namespace +metadata: + name: brochure + labels: + app.kubernetes.io/part-of: flowercore +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: brochure-web + namespace: brochure + labels: + app: brochure-web + app.kubernetes.io/name: brochure-web + app.kubernetes.io/part-of: flowercore +spec: + replicas: 1 + revisionHistoryLimit: 3 + selector: + matchLabels: + app: brochure-web + template: + metadata: + labels: + app: brochure-web + app.kubernetes.io/name: brochure-web + app.kubernetes.io/part-of: flowercore + spec: + containers: + - name: brochure-web + image: localhost/fc-brochure-web:v20260524-sprint32 + imagePullPolicy: Never + ports: + - containerPort: 8080 + name: http + env: + - name: ASPNETCORE_ENVIRONMENT + value: Production + - name: ASPNETCORE_URLS + value: "http://+:8080" + resources: + requests: + cpu: "25m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 30 + periodSeconds: 30 + securityContext: + runAsNonRoot: true + runAsUser: 1654 + runAsGroup: 1654 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: brochure-web + namespace: brochure + labels: + app: brochure-web + app.kubernetes.io/name: brochure-web + app.kubernetes.io/part-of: flowercore +spec: + type: ClusterIP + selector: + app: brochure-web + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: brochure-web-tls + namespace: brochure +spec: + secretName: brochure-web-tls + issuerRef: + name: step-ca-acme + kind: ClusterIssuer + dnsNames: + - brochure.flowercore.io + duration: 720h + renewBefore: 240h +--- +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: brochure-web-public + namespace: brochure +spec: + entryPoints: + - websecure + routes: + - match: Host(`brochure.flowercore.io`) && (Method(`GET`) || Method(`HEAD`)) + kind: Rule + services: + - name: brochure-web + port: 8080 + tls: + secretName: brochure-web-tls diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index dcaeec6..eb9683d 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -13,6 +13,7 @@ public sealed class FleetManifestLintTests private static readonly HashSet PublicReadOnlyHosts = new(StringComparer.Ordinal) { + "brochure.flowercore.io", "dist.flowercore.io", "dns.iamworkin.lan", }; diff --git a/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego b/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego index 691d08a..1422287 100644 --- a/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego +++ b/tests/bluejay-infra-lint/conftest.dev/02_public_method_allowlist.rego @@ -1,6 +1,6 @@ package bluejayinfra.public_method_allowlist -public_hosts := {"dist.flowercore.io", "dns.iamworkin.lan"} +public_hosts := {"brochure.flowercore.io", "dist.flowercore.io", "dns.iamworkin.lan"} deny[msg] { input.kind == "IngressRoute" -- 2.49.1