diff --git a/.github/workflows/deploy-lint.yml b/.github/workflows/deploy-lint.yml index aae3acb..72b29f8 100644 --- a/.github/workflows/deploy-lint.yml +++ b/.github/workflows/deploy-lint.yml @@ -79,7 +79,9 @@ jobs: # alerts file) plus the ServiceMonitor scrape knobs. -ignore-missing-schemas skips the # CRDs themselves, so the guards below assert the CONTENT actually rendered. mon_set="--set metrics.prometheusRule.enabled=true --set metrics.serviceMonitor.enabled=true --set metrics.serviceMonitor.sampleLimit=50000" - for extra in "" "--set console.enabled=true --set console.replicas=2" "$cluster_tls_set" "--set topologySpread.enabled=true" "$np_set" "$k3s_set" "$gd_set" "$mon_set"; do + # The console-UI Ingress needs BOTH console.enabled and console.ingress.enabled. + ing_set="--set console.enabled=true --set console.ingress.enabled=true --set console.ingress.className=traefik" + for extra in "" "--set console.enabled=true --set console.replicas=2" "$cluster_tls_set" "--set topologySpread.enabled=true" "$np_set" "$k3s_set" "$gd_set" "$mon_set" "$ing_set"; do echo "::group::helm template ${extra:-(defaults)}" # $extra must WORD-SPLIT into multiple `--set` flags, so it is # intentionally unquoted (SC2086 does not apply here). @@ -96,6 +98,14 @@ jobs: # propagates -- turning a SUCCESSFUL match into a false failure. A here-string # has no upstream writer to break. case "$extra" in + *console.ingress.enabled=true*) + # Assert the Ingress rendered AND targets the CONSOLE Service (never the cache -- + # RESP is L4 and an HTTP Ingress cannot front it). + grep -q "kind: Ingress" <<< "$rendered" \ + || { echo "ERROR: console.ingress.enabled rendered no Ingress"; exit 1; } + grep -q "name: ic-ironcache-console" <<< "$rendered" \ + || { echo "ERROR: console Ingress does not target the console Service"; exit 1; } + ;; *console.enabled=true*) # Guard against a broken `console.enabled` gate silently rendering # nothing (kubeconform is happy with zero console objects, so assert diff --git a/deploy/K8S_READINESS_PLAN.md b/deploy/K8S_READINESS_PLAN.md index 1f040d9..dc2e2ab 100644 --- a/deploy/K8S_READINESS_PLAN.md +++ b/deploy/K8S_READINESS_PLAN.md @@ -61,11 +61,8 @@ not stale text). The chart is production-safe for **fixed-size** clusters. 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-3 finish** -- the upgrade-path e2e (install -> `helm upgrade` -> re-test), the case that - catches charts which lint clean but fail to *upgrade*. In flight (#763). - 2. **P2-7** a console-UI Ingress template, and **P2-8** the doc-only clarifications (incl. - pinning the busybox init image by digest). - 3. **The day-2 scaling Operator** (Section 5) -- a deliberate non-goal for now; the chart + the + 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 #392 upgrade driver cover fixed-size clusters. --- @@ -113,10 +110,10 @@ not stale text). The chart is production-safe for **fixed-size** clusters. | Prometheus alerts + Grafana dashboard | **DONE** | Dashboard auto-provisioned as a sidecar-discoverable ConfigMap (#758); alert rules shipped as a `PrometheusRule` from the same in-chart file a plain Prometheus can load (`metrics.prometheusRule.enabled`). | | Cache-pods NetworkPolicy | **DONE** | Opt-in policy locking cluster-bus/repl to peers; client + metrics ports stay open by design (the metrics port serves the kubelet probes) (#748). Inert on flannel/k3s-default. | | `values.schema.json` | **DONE** | draft-07: types, enums, port bounds, conditional-requires, and top-level `additionalProperties:false`; doc-only for the non-encodable rules. Negative tests in CI (#756). | -| `helm test` + `ct` (kind/k3d install) | **PARTIAL** | `helm test` PING hook + a kind **install** e2e (#759) -- which caught a real shipped preStop bug on its first run. Still missing: `ct` and, more importantly, an **upgrade** path test (the plan's P2-3 asks for install AND upgrade). | +| `helm test` + `ct` (kind/k3d install) | **DONE (via kind; `ct` not used)** | `helm test` PING hook + a kind e2e covering INSTALL and UPGRADE (#759, #763): install -> `helm test` -> `helm upgrade` (rolls the pod) -> `helm test` again, plus a live cluster-secret-preservation gate for #747 (the `lookup` branch deploy-lint can never reach). `ct` itself is not used -- the kind job covers what P2-3 asked for. #759's first run caught a real shipped preStop bug. | | OCI chart publish + signing | **DONE** | Packaged + pushed to `ghcr.io//charts` and keyless cosign-signed by digest on a release tag, with a clobber guard (#760). | | `appVersion` / default `image.tag` pinned | **DONE** | `appVersion: 0.1.0` drives the tag (`image.tag | default .Chart.AppVersion`); raw manifests pinned too (#755). | -| Ingress template (console UI) | **MISSING** | Still no Ingress template (plan P2-7). RESP is L4 so the TCP path is documented in `deploy/K3S.md`, but the console UI has no templated Ingress. | +| Ingress template (console UI) | **DONE** | `console.ingress.*` renders an Ingress for the console UI only (requires `console.enabled`). RESP is L4, so the cache's TCP path stays documented in `deploy/K3S.md`. | | k3s guidance (local-path/Traefik/servicelb/air-gap) | **DONE** | `deploy/K3S.md` + the `values-k3s.yaml` single-node overlay (#757). | --- diff --git a/deploy/helm/ironcache/templates/console-ingress.yaml b/deploy/helm/ironcache/templates/console-ingress.yaml new file mode 100644 index 0000000..24befc0 --- /dev/null +++ b/deploy/helm/ironcache/templates/console-ingress.yaml @@ -0,0 +1,52 @@ +{{- /* SPDX-License-Identifier: MIT OR Apache-2.0 */ -}} +{{- if and .Values.console.enabled .Values.console.ingress.enabled }} +{{- $httpPort := int .Values.console.httpPort -}} +{{- $svc := include "ironcache.console.fullname" . -}} +# Ingress for the CONSOLE UI ONLY (plain HTTP). Requires console.enabled. +# +# It deliberately does NOT front the cache: RESP is an L4/TCP protocol, so an HTTP Ingress +# cannot proxy it. Expose the cache with the client Service (ClusterIP for in-cluster clients), +# a NodePort/LoadBalancer, or a Traefik `IngressRouteTCP` -- see deploy/K3S.md. And note a +# single L7/L4 endpoint in front of a raft cluster is NOT cluster-aware (nodes answer MOVED for +# slots they do not own), so prefer cluster-aware clients on the headless per-pod DNS. +# +# The console serves the operator UI + API: put real authentication in front of it (an +# ingress-level authn middleware and/or console.tokensSecret), and prefer TLS. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "ironcache.console.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "ironcache.console.labels" . | nindent 4 }} + {{- with .Values.console.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.console.ingress.className }} + # e.g. "traefik" on stock k3s, "nginx" elsewhere. Empty = the cluster's default IngressClass. + ingressClassName: {{ . | quote }} + {{- end }} + {{- with .Values.console.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.console.ingress.hosts }} + - {{- with .host }} + host: {{ . | quote }} + {{- end }} + http: + paths: + {{- range .paths }} + - path: {{ .path | default "/" | quote }} + pathType: {{ .pathType | default "Prefix" }} + backend: + service: + name: {{ $svc }} + port: + number: {{ $httpPort }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/ironcache/values.schema.json b/deploy/helm/ironcache/values.schema.json index 633ab32..5f7a808 100644 --- a/deploy/helm/ironcache/values.schema.json +++ b/deploy/helm/ironcache/values.schema.json @@ -728,6 +728,34 @@ "items": {} } } + }, + "ingress": { + "type": "object", + "description": "Optional Ingress for the console UI (plain HTTP). Cannot front the cache: RESP is L4.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Render the console Ingress (requires console.enabled)." + }, + "className": { + "type": "string", + "description": "IngressClass name, e.g. traefik on k3s. Empty = the cluster default." + }, + "annotations": { + "type": "object", + "description": "Controller-specific annotations, passed through verbatim." + }, + "hosts": { + "type": "array", + "description": "Rules: [{host, paths:[{path,pathType}]}]. host optional (catch-all).", + "items": {} + }, + "tls": { + "type": "array", + "description": "Ingress TLS blocks, passed through verbatim.", + "items": {} + } + } } } } diff --git a/deploy/helm/ironcache/values.yaml b/deploy/helm/ironcache/values.yaml index 507a73a..25aa0e2 100644 --- a/deploy/helm/ironcache/values.yaml +++ b/deploy/helm/ironcache/values.yaml @@ -399,6 +399,25 @@ console: pullPolicy: IfNotPresent # The single HTTP port serving /livez + /readyz + /metrics + the UI + /api/*. httpPort: 9180 + # Optional Ingress for the CONSOLE UI (plain HTTP). It does NOT and cannot front the cache: + # RESP is L4/TCP, so use the client Service / NodePort / LoadBalancer / a Traefik + # IngressRouteTCP for that (see deploy/K3S.md). Put real authn in front of this -- the console + # exposes an operator UI + API. + ingress: + enabled: false + # e.g. "traefik" on stock k3s, "nginx" elsewhere. Empty = the cluster's default IngressClass. + className: "" + # Controller-specific annotations (auth middleware, cert-manager issuer, ...). + annotations: {} + # Each entry: {host: , paths: [{path: /, pathType: Prefix}]}. `host` may be omitted + # for a catch-all rule; path defaults to "/" and pathType to "Prefix". + hosts: + - host: ironcache-console.local + paths: + - path: / + pathType: Prefix + # Verbatim Ingress TLS blocks, e.g. [{secretName: console-tls, hosts: [ironcache-console.local]}]. + tls: [] service: type: ClusterIP # Seed nodes the console polls. Point at the cache client Service DNS, e.g.