From 9b5020dea0b21c3d74a309b70f814820f7c1e625 Mon Sep 17 00:00:00 2001 From: Luxit Date: Sun, 30 Aug 2026 09:58:08 +0200 Subject: [PATCH] Sync the self-hosting stack --- kubernetes/00-namespace.yaml | 5 + kubernetes/01-secret-job.yaml | 28 ++++++ kubernetes/02-config.yaml | 8 ++ kubernetes/03-migrate-job.yaml | 24 ++++- kubernetes/04-api.yaml | 8 ++ kubernetes/05-web.yaml | 16 +++ kubernetes/06-ingress.yaml | 4 + kubernetes/07-disruption.yaml | 8 ++ kubernetes/08-redis-sentinel.yaml | 26 +++++ kubernetes/README.md | 84 ++++++++++++++++ kubernetes/argocd-application.example.yaml | 108 +++++++++++++++++++++ kubernetes/kustomization.yaml | 75 ++++++++++++++ 12 files changed, 392 insertions(+), 2 deletions(-) create mode 100644 kubernetes/argocd-application.example.yaml create mode 100644 kubernetes/kustomization.yaml diff --git a/kubernetes/00-namespace.yaml b/kubernetes/00-namespace.yaml index 6b98f2a..11f434d 100644 --- a/kubernetes/00-namespace.yaml +++ b/kubernetes/00-namespace.yaml @@ -1,6 +1,11 @@ apiVersion: v1 kind: Namespace metadata: + # First, alone. Everything below is namespaced, and a sync that applied them in parallel + # would race the namespace's own creation — including the Pod Security labels above, which + # decide whether any of these pods are admitted at all. + annotations: + argocd.argoproj.io/sync-wave: "-3" name: jarvis labels: # Both images run unprivileged and drop every capability, so the strictest profile applies diff --git a/kubernetes/01-secret-job.yaml b/kubernetes/01-secret-job.yaml index 5ac315f..34025a0 100644 --- a/kubernetes/01-secret-job.yaml +++ b/kubernetes/01-secret-job.yaml @@ -31,12 +31,21 @@ kind: ServiceAccount metadata: name: jarvis-secret-minter namespace: jarvis + # The identity the minting Job below authenticates as, one wave ahead of it. Ordinary + # resources, not hooks: a hook ServiceAccount would be deleted and recreated on every sync, + # invalidating its token for no reason. + annotations: + argocd.argoproj.io/sync-wave: "-2" --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: jarvis-secret-minter namespace: jarvis + # The permission that identity carries, in the same wave as the identity itself: a Role that + # lands after the Job has already authenticated is a Job that got a 403. + annotations: + argocd.argoproj.io/sync-wave: "-2" rules: # `create` AND NOTHING ELSE. No `get`, no `list`, no `update`. # @@ -57,6 +66,9 @@ kind: RoleBinding metadata: name: jarvis-secret-minter namespace: jarvis + # And the binding between the two, in the same wave for the same reason. + annotations: + argocd.argoproj.io/sync-wave: "-2" roleRef: apiGroup: rbac.authorization.k8s.io kind: Role @@ -71,6 +83,22 @@ kind: Job metadata: name: jarvis-mint-secrets namespace: jarvis + annotations: + # A sync hook for the same reason the migration is one — `spec.template` is immutable, so an + # ordinary Job whose image digest moved is a sync that fails on "field is immutable" — plus one + # of its own: `ttlSecondsAfterFinished` deletes this Job an hour after it succeeds. As an + # ordinary tracked resource that is a controller reporting OutOfSync forever, on an object whose + # whole purpose is to be gone. + # + # Running it again is safe BY CONSTRUCTION rather than by care: the identity below can `create` + # and nothing else, so a second run cannot overwrite the secret it wrote the first time. It gets + # a 409 and treats it as success. That is what makes it safe to hand to a controller that will + # reconcile it on a schedule nobody is watching. + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + # Before the migration (wave 0), because the migration reads DATABASE_URL out of the secrets; + # after the ServiceAccount and Role above (wave -2), which this Job authenticates as. + argocd.argoproj.io/sync-wave: "-1" spec: # Re-running is safe (409 → left untouched), so a retry costs nothing. A high limit would only # mean a broken RBAC took longer to become visible. diff --git a/kubernetes/02-config.yaml b/kubernetes/02-config.yaml index 1df3e12..28c49c2 100644 --- a/kubernetes/02-config.yaml +++ b/kubernetes/02-config.yaml @@ -1,6 +1,11 @@ apiVersion: v1 kind: ConfigMap metadata: + # Before anything reads it: the migration Job (wave 0) and both Deployments (wave 1) mount + # these as envFrom, and a pod that starts without them starts with the wrong configuration + # rather than with none — envFrom of a missing ConfigMap is a pod that never becomes ready. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: jarvis-api-config namespace: jarvis data: @@ -65,6 +70,9 @@ data: apiVersion: v1 kind: ConfigMap metadata: + # With the ConfigMap above, for the same reason. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: jarvis-web-config namespace: jarvis data: diff --git a/kubernetes/03-migrate-job.yaml b/kubernetes/03-migrate-job.yaml index 8463852..7144fec 100644 --- a/kubernetes/03-migrate-job.yaml +++ b/kubernetes/03-migrate-job.yaml @@ -10,8 +10,17 @@ # observable status, a bounded number of retries, and a failure that stops the rollout instead of # letting it proceed against a schema that was never applied. # -# With Helm this becomes a `pre-install,pre-upgrade` hook; with Argo, a sync-wave. Applied by hand, -# the ordering is yours to keep — see the README. +# UNDER ARGO CD IT IS A SYNC HOOK, and that is not decoration — it is what makes this directory +# reconcilable at all. A Job's `spec.template` is immutable, so an ordinary Job whose image digest +# changed is a patch the API server REFUSES: the sync fails on "field is immutable" and the release +# stops, every time, from the second release onwards. `BeforeHookCreation` deletes the previous Job +# before creating this one, which is the supported way of saying "run it again, with new contents". +# +# It costs one thing worth knowing: the previous run's Job object, and its logs, are gone by the +# time the next one starts. Ship the logs elsewhere if you need them after the fact. +# +# With Helm this is a `pre-install,pre-upgrade` hook instead. Applied by hand, the ordering is yours +# to keep — see the README. # # It is safe to run twice: every pass is idempotent and the whole phase is wrapped in a Postgres # advisory lock, so a second copy waits rather than racing. @@ -20,6 +29,17 @@ kind: Job metadata: name: jarvis-migrate namespace: jarvis + annotations: + # The SYNC phase, not PreSync. A PreSync hook runs in a phase of its own, before any ordinary + # resource is applied — which on a first sync means before the namespace, the ConfigMap and the + # Secret this Job reads exist. In the Sync phase it is ordered by the same waves as everything + # else, so "after the config, before the api" is one number rather than a second mechanism. + argocd.argoproj.io/hook: Sync + argocd.argoproj.io/hook-delete-policy: BeforeHookCreation + # After the config and the minting (waves -2 and -1), before any api pod (wave 1). Argo does not + # begin the next wave until this Job reports complete, which is the ordering the README's + # `kubectl wait` keeps by hand. + argocd.argoproj.io/sync-wave: "0" spec: # One writer. The lock makes a second one wait rather than corrupt, but there is no reason to # have one waiting. diff --git a/kubernetes/04-api.yaml b/kubernetes/04-api.yaml index b3f7d3c..180a822 100644 --- a/kubernetes/04-api.yaml +++ b/kubernetes/04-api.yaml @@ -1,6 +1,11 @@ apiVersion: apps/v1 kind: Deployment metadata: + # After the schema. This is the wave the whole ordering exists for: an api pod that starts + # against a schema the migration has not applied is the failure every other number here is + # arranged to prevent. + annotations: + argocd.argoproj.io/sync-wave: "1" name: jarvis-api namespace: jarvis spec: @@ -176,6 +181,9 @@ spec: apiVersion: v1 kind: Service metadata: + # With the Deployment it fronts. + annotations: + argocd.argoproj.io/sync-wave: "1" name: jarvis-api namespace: jarvis spec: diff --git a/kubernetes/05-web.yaml b/kubernetes/05-web.yaml index f9f0e2f..45704c5 100644 --- a/kubernetes/05-web.yaml +++ b/kubernetes/05-web.yaml @@ -1,6 +1,16 @@ apiVersion: apps/v1 kind: Deployment metadata: + # Beside the api, in the same wave, and NOT because it is independent of it — measured on a real + # cluster, this pod's `/readyz` is nginx proxying the api's `/api/health/live`, so it answers 502 + # and stays unready for as long as there is no api behind it. + # + # The same wave is still right. Argo waits for a wave as a whole, so the api coming up is what + # makes this ready and there is nothing for a later wave to add; putting it earlier would only + # mean it sat unready for longer. What it does mean is that a stuck api presents as TWO unhealthy + # Deployments rather than one, and the api is the one to look at. + annotations: + argocd.argoproj.io/sync-wave: "1" name: jarvis-web namespace: jarvis spec: @@ -112,6 +122,9 @@ spec: apiVersion: v1 kind: Service metadata: + # With the Deployment it belongs to. + annotations: + argocd.argoproj.io/sync-wave: "1" name: jarvis-web namespace: jarvis spec: @@ -134,6 +147,9 @@ spec: apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: + # With the Deployment it belongs to. + annotations: + argocd.argoproj.io/sync-wave: "1" name: jarvis-web namespace: jarvis spec: diff --git a/kubernetes/06-ingress.yaml b/kubernetes/06-ingress.yaml index db64e77..bec1abe 100644 --- a/kubernetes/06-ingress.yaml +++ b/kubernetes/06-ingress.yaml @@ -15,6 +15,10 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: + # Last, so that the first traffic admitted from outside meets Services with endpoints behind + # them. An Ingress applied early is a route to a 503. + annotations: + argocd.argoproj.io/sync-wave: "2" name: jarvis namespace: jarvis annotations: diff --git a/kubernetes/07-disruption.yaml b/kubernetes/07-disruption.yaml index 06e2839..df5f14b 100644 --- a/kubernetes/07-disruption.yaml +++ b/kubernetes/07-disruption.yaml @@ -6,6 +6,11 @@ apiVersion: policy/v1 kind: PodDisruptionBudget metadata: + # With the Ingress, after the Deployments they protect. A PodDisruptionBudget naming a + # selector that matches nothing is not an error and not a warning — it is simply not + # protecting anything, which is the one state it must never be in quietly. + annotations: + argocd.argoproj.io/sync-wave: "2" name: jarvis-api namespace: jarvis spec: @@ -18,6 +23,9 @@ spec: apiVersion: policy/v1 kind: PodDisruptionBudget metadata: + # Same wave, same reason. + annotations: + argocd.argoproj.io/sync-wave: "2" name: jarvis-web namespace: jarvis spec: diff --git a/kubernetes/08-redis-sentinel.yaml b/kubernetes/08-redis-sentinel.yaml index 24050c5..f41caa0 100644 --- a/kubernetes/08-redis-sentinel.yaml +++ b/kubernetes/08-redis-sentinel.yaml @@ -23,6 +23,11 @@ apiVersion: v1 kind: ConfigMap metadata: + # Beside the config, well before the api. The api reaches Redis on its readiness probe, so a + # keyspace that is not there yet is a Deployment that rolls and then reports unready for as + # long as it takes — which Argo would read as a failed sync rather than as a wait. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis-scripts namespace: jarvis data: @@ -79,6 +84,9 @@ data: apiVersion: v1 kind: ConfigMap metadata: + # With the rest of Redis, in the wave before anything reads it. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis-config namespace: jarvis data: @@ -112,6 +120,9 @@ data: apiVersion: v1 kind: Service metadata: + # With the rest of Redis, in the wave before anything reads it. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis namespace: jarvis spec: @@ -129,6 +140,9 @@ spec: apiVersion: v1 kind: Service metadata: + # With the rest of Redis, in the wave before anything reads it. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis-sentinel namespace: jarvis spec: @@ -144,6 +158,9 @@ spec: apiVersion: apps/v1 kind: StatefulSet metadata: + # With the rest of Redis, in the wave before anything reads it. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis namespace: jarvis spec: @@ -232,6 +249,9 @@ spec: apiVersion: apps/v1 kind: StatefulSet metadata: + # With the rest of Redis, in the wave before anything reads it. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis-sentinel namespace: jarvis spec: @@ -321,6 +341,9 @@ spec: apiVersion: policy/v1 kind: PodDisruptionBudget metadata: + # With the rest of Redis, in the wave before anything reads it. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis-sentinel namespace: jarvis spec: @@ -332,6 +355,9 @@ spec: apiVersion: policy/v1 kind: PodDisruptionBudget metadata: + # With the rest of Redis, in the wave before anything reads it. + annotations: + argocd.argoproj.io/sync-wave: "-2" name: redis namespace: jarvis spec: diff --git a/kubernetes/README.md b/kubernetes/README.md index 5cecb76..69bcd10 100644 --- a/kubernetes/README.md +++ b/kubernetes/README.md @@ -97,6 +97,90 @@ about the install screen applies unchanged — including that **whoever reaches instance first owns it**, with no deadline. Point a public hostname at this after you have installed, not before. +## GitOps: Argo CD, Flux, or anything that reconciles + +`kustomization.yaml` is the entry point. Point a controller at this directory and it renders every +manifest here except one — and `argocd-application.example.yaml` is a worked Application to copy. + +```sh +kubectl apply -k . # the same thing, by hand +``` + +**The exception is `01-secret.example.yaml`, and it is left out on purpose.** It is a template full +of placeholders. A controller applies what it is pointed at, continuously, without asking — so a +Secret full of `REPLACE_ME` inside the source of truth is a controller that overwrites your real +credentials with placeholders on its next reconcile and puts them back every time you fix them. The +api cannot reach its database, the vault key is wrong, and nothing about the sync looks unhealthy +while it happens. Write `jarvis-secrets` by hand, or through SealedSecrets, ExternalSecrets or +Vault, whose ciphertext is safe to commit. `jarvis-generated` is minted in-cluster and never exists +in git at all. + +### The order is in the manifests, not in the filenames + +A controller reads this directory as a set; `00-`, `01-` and `02-` mean nothing to it. The order +that the `kubectl apply` sequence above keeps by hand is carried by +`argocd.argoproj.io/sync-wave` annotations on the resources themselves, so that it lives beside the +thing being ordered: + +| wave | what | why it cannot be later | +| --- | --- | --- | +| `-3` | Namespace | it carries the Pod Security labels that decide whether any of these pods are admitted | +| `-2` | ConfigMaps, the minter's RBAC, Redis | everything below reads the config; the api's readiness probe reaches Redis | +| `-1` | `jarvis-mint-secrets` **(hook)** | the migration reads `DATABASE_URL` out of what it writes | +| `0` | `jarvis-migrate` **(hook)** | an api pod against an unmigrated schema is the failure all of this prevents | +| `1` | api and web Deployments, Services | the web pod's `/readyz` proxies the api's health endpoint, so it cannot be ready before the api is | +| `2` | Ingress, PodDisruptionBudgets | an Ingress applied early is a route to a 503 | + +Argo does not begin a wave until the previous one reports healthy, which is what the `kubectl wait` +lines are doing by hand. + +### Why the two Jobs are hooks + +Without this, the directory works exactly once. **A Job's `spec.template` is immutable**, so the +second release — the first one with a different image digest — is a patch the API server refuses +with `field is immutable`, and the sync fails there and stays failed. `hook: Sync` with +`hook-delete-policy: BeforeHookCreation` deletes the previous Job before creating the new one, which +is the supported way to say "run it again, with new contents". + +The Sync phase rather than PreSync, deliberately: a PreSync hook runs before any ordinary resource +is applied, which on a first sync is before the namespace, the ConfigMap and the Secret these Jobs +read exist. In the Sync phase they are ordered by the same waves as everything else. + +Two consequences worth knowing: + +- **The previous run's Job object, and its logs, are gone** by the time the next one starts. Ship + the logs if you need them after the fact. +- **Every sync runs the migration.** That is safe — the phase is idempotent and holds a Postgres + advisory lock — and it is why the example Application leaves `automated` sync OFF. A commit that + applies a schema change with nobody watching turns the one thing this directory works hardest to + make visible, a migration that REFUSES because it would drop a column, into a notification. + +### Pin the images in one place + +`kustomization.yaml` carries an `images:` block, so the three digests are set once rather than in +three files. Three, not two: `jarvis-agent-dist` is an initContainer in `04-api.yaml` and is easy to +miss, and missing it renders cleanly while the api pod never starts. + +A version bump is then those lines and nothing else. + +### What most estates should actually do + +Point the Application at **your** repository, not at this one, with this directory as a remote base: + +```yaml +# your-repo/jarvis/kustomization.yaml +resources: + - https://git.luxit.be/Luxit/jarvis-selfhost//kubernetes?ref=v1.2.3 +images: + - name: git.luxit.be/luxit/jarvis-api + digest: sha256:... +patches: + - path: replicas.yaml # your node count, your ingress host, your storage class +``` + +Then the digests you run are in a commit you control, upgrading is a diff you can review and revert, +and `?ref=` pins the base to a tag rather than a branch that moves under you. Pin it. + ## The three things most likely to bite **Never mint secrets per pod.** On Compose a one-shot generates the vault key and the JWT secrets diff --git a/kubernetes/argocd-application.example.yaml b/kubernetes/argocd-application.example.yaml new file mode 100644 index 0000000..29f03dc --- /dev/null +++ b/kubernetes/argocd-application.example.yaml @@ -0,0 +1,108 @@ +# An Argo CD Application pointing at this directory. EXAMPLE — read it, do not apply it unedited. +# +# It is `.example.` for the same reason `01-secret.example.yaml` is: it names a repository that is +# not yours. Copy it into whatever repository holds your Argo Applications, change the three values +# marked below, and apply it there. +# +# ─── WHAT TO POINT IT AT ────────────────────────────────────────────────────────────────────── +# +# You have two honest choices, and the second is the one to grow into. +# +# DIRECTLY AT THIS DIRECTORY, as written below. Simplest, and it works — but the image digests live +# in `kustomization.yaml` here, so pinning a version means committing to a repository you do not +# own. Fine for reading, for a first cluster, and for tracking `main` on a lab estate. +# +# OR AS A BASE UNDER YOUR OWN OVERLAY, which is what most estates end up wanting: +# +# # your-repo/jarvis/kustomization.yaml +# apiVersion: kustomize.config.k8s.io/v1beta1 +# kind: Kustomization +# resources: +# - https://git.luxit.be/Luxit/jarvis-selfhost//kubernetes?ref=main +# images: +# - name: git.luxit.be/luxit/jarvis-api +# digest: sha256: +# - name: git.luxit.be/luxit/jarvis-web +# digest: sha256: +# patches: +# - path: replicas.yaml # your node count, your ingress host, your storage class +# +# Then the Application points at YOUR repository, your digests are in a commit you control, and +# upgrading is a one-line diff you can review and revert. `?ref=` pins the base to a tag or a commit +# rather than tracking a branch that moves under you — pin it. +# +# ─── WHAT IS NOT IN GIT, AND MUST NOT BE ────────────────────────────────────────────────────── +# +# `jarvis-secrets`. See the note at the top of `kustomization.yaml`: a controller that reconciles a +# Secret full of placeholders will overwrite your real credentials with them, repeatedly, and +# nothing about the sync will look unhealthy while it does. Write it by hand or through +# SealedSecrets / ExternalSecrets / Vault, whose ciphertext IS safe to commit. +# +# `jarvis-generated` is minted in-cluster by the hook in `01-secret-job.yaml` and never exists in +# git at all. Back the vault key up out of the cluster the first time it is minted — see that file. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: jarvis + namespace: argocd + # Argo deletes an Application's resources when the Application is deleted. On something holding a + # database's credentials and a vault key, that should be a deliberate act rather than a + # consequence of `kubectl delete app jarvis` — so the finalizer is left OFF and pruning of the + # namespace is not automatic. Add it when you have decided that is what you want: + # + # finalizers: + # - resources-finalizer.argocd.argoproj.io +spec: + project: default + + source: + # CHANGE ME — your repository, or the public one if you are only reading. + repoURL: https://git.luxit.be/Luxit/jarvis-selfhost.git + # CHANGE ME — a tag or a commit, not a branch. A branch moves; a cluster that follows one is a + # cluster whose version is decided by somebody else's merge. + targetRevision: main + path: kubernetes + + destination: + server: https://kubernetes.default.svc + # Matches the namespace the manifests declare. They carry it explicitly, so this is agreement + # rather than instruction. + namespace: jarvis + + syncPolicy: + # DELIBERATELY MANUAL, and this is the one setting to think hardest about before changing. + # + # Every sync runs the migration hook against your production schema. Automated, that means a + # commit to the tracked revision applies a schema change with nobody watching — and the one + # thing this whole directory is arranged to make visible (a migration that REFUSES because it + # would drop a column) becomes a failed sync notification instead of a person deciding. + # + # Turn it on once you trust the pipeline in front of it, not before: + # + # automated: + # prune: true + # selfHeal: true + + # NOTHING HERE, and the absence is deliberate in one specific place. + # + # `CreateNamespace=true` is the option most Applications carry, and this one must not. The + # namespace is `00-namespace.yaml` at wave -3, and it carries the Pod Security labels that + # decide whether any of these pods are admitted at all. A namespace Argo creates for you has no + # labels — so the first sync would admit pods under no policy and enforce `restricted` a moment + # later, which is the one ordering this directory exists to get right. + # + # syncOptions: [] + + # The HorizontalPodAutoscaler in `05-web.yaml` targets the web Deployment, which also declares + # `replicas: 2`. Once the autoscaler acts, git and the cluster disagree about that field forever — + # and it is the autoscaler working, not drift. Without this, a `selfHeal` policy and the HPA push + # the replica count back and forth at each other indefinitely. + # + # The api Deployment is deliberately NOT here: nothing autoscales it, so git owns its replica + # count and a difference there is a real one worth showing. + ignoreDifferences: + - group: apps + kind: Deployment + name: jarvis-web + jsonPointers: + - /spec/replicas diff --git a/kubernetes/kustomization.yaml b/kubernetes/kustomization.yaml new file mode 100644 index 0000000..b6be1d2 --- /dev/null +++ b/kubernetes/kustomization.yaml @@ -0,0 +1,75 @@ +# The entry point for `kubectl apply -k` and for Argo CD. +# +# It exists so that a GitOps tool has ONE path to point at, and so that the two images are pinned in +# ONE place instead of three. The manifests beside it are unchanged and still apply individually +# with `kubectl apply -f` — this aggregates them, it does not replace them. +# +# ─── WHAT IS DELIBERATELY NOT HERE ──────────────────────────────────────────────────────────── +# +# `01-secret.example.yaml`, and its absence is the most important line in this file. +# +# It is a TEMPLATE carrying placeholder values. A GitOps controller applies what it is pointed at, +# continuously, without asking — so a Secret full of `REPLACE_ME` inside the source of truth is a +# controller that will overwrite your real credentials with placeholders on its next reconcile, and +# then keep putting them back every time you fix them. The api would fail to reach its database, the +# vault key would be wrong, and nothing in the sync would look unhealthy. +# +# Write `jarvis-secrets` into the cluster by another route entirely: by hand, or through +# SealedSecrets, ExternalSecrets or Vault — anything whose ciphertext is safe to commit. The +# generated half (`jarvis-generated`) is minted in-cluster by the hook in `01-secret-job.yaml` and +# is likewise never in git. +# +# ─── ORDER ──────────────────────────────────────────────────────────────────────────────────── +# +# Filenames do not order anything: `kubectl apply -k` and Argo both read this list as a set. The +# order is carried by `argocd.argoproj.io/sync-wave` annotations on the resources themselves, so +# that it lives beside the thing being ordered rather than in a list somebody has to keep in step. +# See the table in README.md. +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - 00-namespace.yaml + # The identity that mints the generated secrets, and the Job that uses it. The Job is a sync hook; + # its ServiceAccount, Role and RoleBinding are ordinary resources in an earlier wave. + - 01-secret-job.yaml + - 02-config.yaml + - 03-migrate-job.yaml + - 04-api.yaml + - 05-web.yaml + - 06-ingress.yaml + - 07-disruption.yaml + # Remove this line if you point REDIS_URL at a managed Redis. It deploys three Redis processes and + # three sentinels; see the README on what that costs on three nodes. + - 08-redis-sentinel.yaml + +# ─── THE THREE IMAGES ─────────────────────────────────────────────────────────────────────────── +# +# By DIGEST, not by tag, and the manifests carry `@sha256:REPLACE_ME` so that an unedited copy fails +# loudly rather than quietly pulling something. +# +# A tag is a name that can be repointed; a digest is the bytes. It matters more here than in most +# places because the migration Job and the api Deployment must be the SAME BUILD — a schema applied +# by one version and served by another is the failure this whole ordering exists to prevent, and two +# copies of a mutable tag pulled a minute apart can be two different images. +# +# `dev` and `stable` are channels, so they are exactly the kind of name not to pin to. Resolve one +# to its digest and put that here: +# +# docker buildx imagetools inspect git.luxit.be/luxit/jarvis-api:stable --format '{{.Manifest.Digest}}' +# +# This is also the whole of a version bump: change these lines, commit, and let the controller do +# the rest. +# +# THREE, not two. `jarvis-agent-dist` is easy to miss — it is an initContainer in 04-api.yaml rather +# than a container of its own — and missing it is the worst possible half-fix: two images pinned +# from here and one left carrying `REPLACE_ME` in the file, so the render succeeds and the api pod +# never starts. It is also the one that may legitimately be absent: leaving that initContainer out +# is supported, and the installer then answers 503 saying no agent build is published. +images: + - name: git.luxit.be/luxit/jarvis-api + digest: sha256:REPLACE_ME + - name: git.luxit.be/luxit/jarvis-web + digest: sha256:REPLACE_ME + - name: git.luxit.be/luxit/jarvis-agent-dist + digest: sha256:REPLACE_ME