diff --git a/.github/workflows/chart-e2e.yml b/.github/workflows/chart-e2e.yml index 2f341ebb..3678e9e6 100644 --- a/.github/workflows/chart-e2e.yml +++ b/.github/workflows/chart-e2e.yml @@ -27,14 +27,14 @@ jobs: steps: - uses: actions/checkout@v4 - name: Create a kind cluster - uses: helm/kind-action@v1.10.0 + uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 with: # Pin the k8s version to 1.30 so the run is deterministic AND exercises the native # SleepAction preStop path (beta/default-on from 1.30; the chart gates it on >=1.30). # The action's default is 1.29, where SleepAction is alpha-off -- a moving, wrong target. version: v0.23.0 node_image: kindest/node:v1.30.0 - - uses: azure/setup-helm@v4 + - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: version: ${{ env.HELM_VERSION }} - name: Install (standalone) + wait + helm test diff --git a/.github/workflows/chart-release.yml b/.github/workflows/chart-release.yml index 97575c54..d4ed3ce4 100644 --- a/.github/workflows/chart-release.yml +++ b/.github/workflows/chart-release.yml @@ -38,11 +38,11 @@ jobs: id-token: write # keyless cosign signing via OIDC steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: azure/setup-helm@v4 + - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: version: "v3.15.4" - name: Install cosign - uses: sigstore/cosign-installer@v3.7.0 + uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 - name: helm lint + package id: pkg run: | diff --git a/.github/workflows/deploy-lint.yml b/.github/workflows/deploy-lint.yml index 72b29f87..f43d973f 100644 --- a/.github/workflows/deploy-lint.yml +++ b/.github/workflows/deploy-lint.yml @@ -40,7 +40,7 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v4 - - uses: azure/setup-helm@v4 + - uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 with: version: ${{ env.HELM_VERSION }} - name: helm lint (defaults + console enabled) diff --git a/deploy/K8S_READINESS_PLAN.md b/deploy/K8S_READINESS_PLAN.md index dc2e2abd..f8603c73 100644 --- a/deploy/K8S_READINESS_PLAN.md +++ b/deploy/K8S_READINESS_PLAN.md @@ -60,9 +60,11 @@ not stale text). The chart is production-safe for **fixed-size** clusters. on its first run it caught + fixed a real shipped preStop-gate bug (SleepAction is beta/default-on only from **1.30**, not 1.29), but `ct` and an **upgrade**-path test are still missing. The chart is now 0.3.0. -- **Genuinely remaining** (each also flagged in the Section 2 table): - 1. **P2-8** the doc-only clarifications (incl. pinning the busybox init image by digest). - 2. **The day-2 scaling Operator** (Section 5) -- a deliberate non-goal for now; the chart + the +- **P2 is now COMPLETE (P2-1..P2-8)**: P2-3 finished with the upgrade-path e2e (#763), P2-6 the + ServiceMonitor scrape knobs + the alert rules as a PrometheusRule (#764), P2-7 the console-UI + Ingress (#765), P2-8 the doc clarifications + the opt-in busybox digest pin. +- **Genuinely remaining** (also flagged in the Section 2 table): + 1. **The day-2 scaling Operator** (Section 5) -- a deliberate non-goal for now; the chart + the #392 upgrade driver cover fixed-size clusters. --- diff --git a/deploy/compose/docker-compose.cluster.yml b/deploy/compose/docker-compose.cluster.yml index 841d1af6..6a440f47 100644 --- a/deploy/compose/docker-compose.cluster.yml +++ b/deploy/compose/docker-compose.cluster.yml @@ -29,7 +29,7 @@ name: ironcache-cluster services: ironcache-1: - image: ghcr.io/elares/ironcache:latest + image: ghcr.io/elares/ironcache:${IRONCACHE_VERSION:-0.1.0} hostname: ironcache-1 command: - server @@ -60,7 +60,7 @@ services: restart: unless-stopped ironcache-2: - image: ghcr.io/elares/ironcache:latest + image: ghcr.io/elares/ironcache:${IRONCACHE_VERSION:-0.1.0} hostname: ironcache-2 command: - server @@ -85,7 +85,7 @@ services: restart: unless-stopped ironcache-3: - image: ghcr.io/elares/ironcache:latest + image: ghcr.io/elares/ironcache:${IRONCACHE_VERSION:-0.1.0} hostname: ironcache-3 command: - server diff --git a/deploy/compose/docker-compose.console.yml b/deploy/compose/docker-compose.console.yml index f8149ddd..5994783b 100644 --- a/deploy/compose/docker-compose.console.yml +++ b/deploy/compose/docker-compose.console.yml @@ -23,7 +23,7 @@ name: ironcache-console # The common console replica config (both replicas are identical). x-console: &console - image: ghcr.io/elares/ironcache-console:latest + image: ghcr.io/elares/ironcache-console:${IRONCACHE_VERSION:-0.1.0} command: [run] environment: IRONCACHE_CONSOLE_HTTP_ADDR: 0.0.0.0:9180 diff --git a/deploy/compose/docker-compose.yml b/deploy/compose/docker-compose.yml index 86c9edb0..e6122068 100644 --- a/deploy/compose/docker-compose.yml +++ b/deploy/compose/docker-compose.yml @@ -18,7 +18,7 @@ name: ironcache services: ironcache: - image: ghcr.io/elares/ironcache:latest + image: ghcr.io/elares/ironcache:${IRONCACHE_VERSION:-0.1.0} command: - server - --bind diff --git a/deploy/helm/ironcache/templates/statefulset.yaml b/deploy/helm/ironcache/templates/statefulset.yaml index ae92e350..57477e15 100644 --- a/deploy/helm/ironcache/templates/statefulset.yaml +++ b/deploy/helm/ironcache/templates/statefulset.yaml @@ -103,7 +103,8 @@ spec: # ordinal) into /run/ironcache/ironcache.toml, which the main container # then passes to --config. Keeps the runtime image distroless/shell-free. - name: stamp-identity - image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + {{- /* digest wins when set (supply-chain pin); note the `@` form, not `:`. */}} + image: "{{ .Values.initImage.repository }}{{ if .Values.initImage.digest }}@{{ .Values.initImage.digest }}{{ else }}:{{ .Values.initImage.tag }}{{ end }}" imagePullPolicy: {{ .Values.initImage.pullPolicy }} command: ["/bin/sh", "/scripts/stamp-identity.sh"] securityContext: diff --git a/deploy/helm/ironcache/values.schema.json b/deploy/helm/ironcache/values.schema.json index 5f7a8081..f3217572 100644 --- a/deploy/helm/ironcache/values.schema.json +++ b/deploy/helm/ironcache/values.schema.json @@ -567,6 +567,10 @@ "Never" ], "description": "Init image pull policy." + }, + "digest": { + "type": "string", + "description": "Optional image digest (sha256:...) for the init image; when set it WINS over tag and renders repo@digest. Empty by default so air-gapped pre-loaded images still work." } } }, diff --git a/deploy/helm/ironcache/values.yaml b/deploy/helm/ironcache/values.yaml index 25aa0e2a..b5d072ca 100644 --- a/deploy/helm/ironcache/values.yaml +++ b/deploy/helm/ironcache/values.yaml @@ -57,7 +57,12 @@ persistence: # The PVC mount path == data_dir. mountPath: /var/lib/ironcache size: 10Gi - # storageClassName: "" leaves the cluster default; set an SSD class in prod. + # storageClassName: "" leaves the cluster default; set an SSD class in prod. NOTE the PV + # RECLAIM POLICY comes from the StorageClass, not this chart: for production use a class whose + # reclaimPolicy is `Retain`, so deleting the PVC does not also destroy the underlying volume + # (the chart's persistentVolumeClaimRetentionPolicy protects the PVC; the StorageClass protects + # the PV behind it). Also: labels/annotations on a volumeClaimTemplate are applied at PVC + # CREATION only -- editing them later does NOT propagate to already-created PVCs. storageClassName: "" accessModes: - ReadWriteOnce @@ -289,6 +294,9 @@ pdb: # compatible with `restricted` because it needs no in-image binary. # # The image already runs as nonroot (uid 65532); fsGroup makes the PVC writable by it. +# runAsUser/runAsGroup MUST match the image's baked-in UID/GID (65532). Changing them to an id +# the image does not own makes the process unable to read its own files (and, with fsGroup left +# behind, the PVC too) -- change these only if you also rebuild the image. podSecurityContext: runAsNonRoot: true runAsUser: 65532 @@ -302,6 +310,9 @@ podSecurityContext: type: RuntimeDefault containerSecurityContext: allowPrivilegeEscalation: false + # readOnlyRootFilesystem applies to `/` ONLY. The pod still has the writable mounts it needs: + # the data PVC at persistence.mountPath and a small emptyDir at /run/ironcache that the init + # container writes the stamped config into. That combination is by design, not a leak. readOnlyRootFilesystem: true capabilities: drop: @@ -378,6 +389,14 @@ topologySpread: initImage: repository: busybox tag: "1.36" + # SUPPLY-CHAIN HARDENING (opt-in): pin the init image by DIGEST instead of the mutable tag. + # When set it WINS over `tag` and renders `busybox@sha256:...`. The verified multi-arch + # (amd64 + arm64 + more) manifest-list digest for busybox:1.36 is: + # sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 + # Left EMPTY by default deliberately: an AIR-GAPPED cluster pre-loads its own busybox tarball + # (see deploy/K3S.md), whose digest will differ, and a digest pin would then fail the pull. + # Set it wherever you control the registry. + digest: "" pullPolicy: IfNotPresent # --- Console (epic #352, issue #363) ------------------------------------------