diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f1d793b..daacce1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -88,7 +88,7 @@ jobs:
cache: true
- name: Install gosec
- run: go install github.com/securego/gosec/v2/cmd/gosec@v2.29.0
+ run: go install github.com/securego/gosec/v2/cmd/gosec@v2.30.0
# Note: do not use -quiet here: it suppresses the report output
# entirely, including the -out SARIF file (the file is then never
@@ -105,6 +105,20 @@ jobs:
with:
sarif_file: results.sarif
+ # -- Markdown docs: Mermaid diagram syntax --
+ # Uses the Node.js preinstalled on the runner (the check needs >= 20.11).
+ docs:
+ name: Docs (Mermaid)
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v7
+
+ - name: Check Mermaid diagrams
+ run: make check-mermaid
+
# -- Helm lint & package --
helm:
name: Helm lint
@@ -120,6 +134,21 @@ jobs:
- name: Set up Helm
uses: azure/setup-helm@v5
+ # On a tag push, bake the tag into the chart before it is packaged and
+ # pushed: appVersion and codx image tag take the git tag, and the chart
+ # patch version is bumped (version: X.Y.Z -> X.Y.Z+1). Branch and PR
+ # builds package the chart as committed.
+ - name: Stamp release tag into chart
+ if: startsWith(github.ref, 'refs/tags/')
+ run: |
+ tag="${GITHUB_REF_NAME}"
+ sed -i -E "s|^appVersion: \".*\"$|appVersion: \"${tag}\"|" charts/codx/Chart.yaml
+ awk -F. -v OFS=. '/^version:/ { $(NF) = $(NF) + 1 } { print }' \
+ charts/codx/Chart.yaml > charts/codx/.Chart.yaml.tmp
+ mv charts/codx/.Chart.yaml.tmp charts/codx/Chart.yaml
+ sed -i -E "0,/^ tag: .*/s// tag: \"${tag}\"/" charts/codx/values.yaml
+ echo "appVersion and image.tag set to ${tag}, chart version: $(sed -n 's/^version: //p' charts/codx/Chart.yaml)"
+
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v4
@@ -142,7 +171,7 @@ jobs:
--set oidc.clientSecret=secret \
--set oidc.redirectUrl=https://codx.example.com/auth/callback \
--set redis.host=valkey:6379 \
- --set ingress.host=codx.example.com \
+ --set ingress.hostname=codx.example.com \
> /dev/null
- name: Helm package
diff --git a/AGENTS.md b/AGENTS.md
index df996a4..5894af0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -26,6 +26,8 @@ for the ground rules and [docs/](docs/) for the feature documentation.
## Language and style
- **English everywhere**: code, comments, commit messages, docs, chart values.
+- Diagrams in Markdown docs are Mermaid (` ```mermaid ` fenced blocks), not
+ ASCII art.
- `gofmt` clean; comments explain *why*, not *what*; minimal diffs that match
the surrounding style.
- Conventional commits (`feat:`, `fix:`, `docs:`, `chore:`), as used in the
@@ -58,7 +60,7 @@ gofmt -l internal/ cmd/ api/ # empty output = clean
### SAST (gosec) - security-sensitive or Go changes
```sh
-go install github.com/securego/gosec/v2/cmd/gosec@v2.29.0 # version pinned in CI
+go install github.com/securego/gosec/v2/cmd/gosec@v2.30.0 # version pinned in CI
"$(go env GOPATH)/bin/gosec" -fmt=sarif -out=results.sarif ./... # exit 0 = clean
```
@@ -106,6 +108,17 @@ make check-values-docs
- When the CRD (`charts/codx/crds/`) changes, regenerate the docs with
`make docs-crd`.
+### Docs (Markdown changes)
+
+```sh
+make check-mermaid
+```
+
+- Validates the syntax of every ` ```mermaid ` block in the Markdown files
+ with the real Mermaid parser (`hack/check_mermaid.mjs`, Node + jsdom, no
+ browser). Diagrams in docs are Mermaid, never ASCII art. The node_modules
+ are installed into a throwaway directory, not the repository.
+
### Workflows (changes under `.github/`)
Validate the YAML parses (`python3 -c "import yaml; yaml.safe_load(open(...))"`)
@@ -117,13 +130,14 @@ A change is only done when these pass - they mirror the local commands above:
- **go**: `go vet` + `go test -race` with coverage
- **lint**: golangci-lint
-- **security**: gosec v2.29.0, SARIF uploaded to code scanning (needs
+- **security**: gosec v2.30.0, SARIF uploaded to code scanning (needs
`security-events: write` + `actions: read`)
- **helm**: dependency update, lint, template render check, package, OCI push
+ cosign sign
- **docker**: amd64 build -> Trivy CVE gate (HIGH/CRITICAL, ignore-unfixed)
-> CycloneDX SBOM (artifact + cosign attest) -> multi-arch build/push ->
cosign sign. The image is scanned **before** it is pushed.
+- **docs**: Mermaid diagram syntax check (`make check-mermaid`).
Pull requests are reviewed by the code owners (`.github/CODEOWNERS`).
diff --git a/Dockerfile b/Dockerfile
index 1ba5a37..93c4965 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# -- Build stage --
-FROM golang:1.27-alpine AS builder
+FROM golang:1.27.3-alpine AS builder
WORKDIR /src
diff --git a/Makefile b/Makefile
index f25016d..60d963f 100644
--- a/Makefile
+++ b/Makefile
@@ -52,6 +52,19 @@ docs-crd:
check-values-docs:
python3 hack/check_values_docs.py charts/codx/values.yaml
+# Validate the syntax of the mermaid diagrams in the Markdown files with
+# the real Mermaid parser (syntax only, no rendering). The node_modules are
+# installed into a throwaway directory so the repository stays free of
+# JavaScript dependencies. Requires node and npm on PATH.
+# ":=" expands mktemp once: a lazy "?=" would run it per reference and the
+# install and the NODE_PATH would point at two different directories.
+MERMAID_CHECK_DIR := $(shell mktemp -d)
+
+.PHONY: check-mermaid
+check-mermaid:
+ npm --prefix $(MERMAID_CHECK_DIR) install --no-audit --no-fund --silent mermaid@11 jsdom >/dev/null
+ NODE_PATH=$(MERMAID_CHECK_DIR)/node_modules node hack/check_mermaid.mjs
+
# Scan the Go modules (go.mod/go.sum) for known CVEs, like the CI does on
# the Docker image. Requires trivy on PATH (https://trivy.dev).
# Fails on HIGH/CRITICAL findings with a fix available.
diff --git a/charts/codx/Chart.lock b/charts/codx/Chart.lock
index 874d100..f88646f 100644
--- a/charts/codx/Chart.lock
+++ b/charts/codx/Chart.lock
@@ -4,6 +4,6 @@ dependencies:
version: 2.41.0
- name: valkey
repository: oci://ghcr.io/valkey-io/valkey-helm
- version: 0.12.0
-digest: sha256:bdfd5df0224e07bb230f7efdd218dbe959a02dbc9c7f3fad52b19295cfb559d4
-generated: "2026-10-13T15:21:42.428651669+02:00"
+ version: 0.12.1
+digest: sha256:12e757ddc2f8f1122d79ac7b077d577ba8a7ff328912c54d63a6b34b79d0a48a
+generated: "2026-10-28T17:43:33.664164823+01:00"
diff --git a/charts/codx/Chart.yaml b/charts/codx/Chart.yaml
index 5ab9696..0f49016 100644
--- a/charts/codx/Chart.yaml
+++ b/charts/codx/Chart.yaml
@@ -28,7 +28,7 @@ dependencies:
repository: oci://ghcr.io/valkey-io/valkey-helm
tags:
- valkey
- version: 0.12.0
+ version: 0.12.1
annotations:
category: DeveloperTools
artifacthub.io/images: |
diff --git a/charts/codx/README.md b/charts/codx/README.md
index 62afa70..defe506 100644
--- a/charts/codx/README.md
+++ b/charts/codx/README.md
@@ -16,7 +16,7 @@ graph TB
subgraph K8s["Kubernetes Cluster"]
subgraph IngressNS["traefik Namespace"]
- Traefik[Traefik Ingress
TLS termination]
+ Traefik[Traefik IngressRoute
TLS termination]
end
subgraph MonitoringNS["kube-prometheus-stack Namespace
Optional"]
@@ -112,6 +112,7 @@ $ helm install my-release oci://registry-1.docker.io/captnbp/codx
- Helm 3.2.0+
- PV provisioner support in the underlying infrastructure
- [cert-manager](https://cert-manager.io/)
+- [Traefik](https://traefik.io/) 3.6+ with the Kubernetes CRD provider (IngressRoute, Middleware, TLSOption, ServersTransport)
## Installing the Chart
@@ -219,38 +220,41 @@ $ helm delete --purge my-release
### Exposure parameters
-| Name | Description | Value |
-| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
-| `service.type` | Kubernetes service type | `ClusterIP` |
-| `service.ports.https` | codx service HTTPS port | `8443` |
-| `service.nodePorts` | Specify the nodePort values for the LoadBalancer and NodePort service types. | `{}` |
-| `service.sessionAffinity` | Control where client requests go, to the same pod or round-robin | `None` |
-| `service.sessionAffinityConfig` | Additional settings for the sessionAffinity | `{}` |
-| `service.clusterIP` | codx service clusterIP IP | `""` |
-| `service.loadBalancerIP` | loadBalancerIP for the CodX service (optional, cloud specific) | `""` |
-| `service.loadBalancerSourceRanges` | Address that are allowed when service is LoadBalancer | `[]` |
-| `service.externalTrafficPolicy` | Enable client source IP preservation | `Cluster` |
-| `service.annotations` | Additional custom annotations for codx service | `{}` |
-| `service.extraPorts` | Extra port to expose on codx service | `[]` |
-| `service.extraHeadlessPorts` | Extra ports to expose on codx headless service | `[]` |
-| `service.ipFamilyPolicy` | Controller Service ipFamilyPolicy (optional, cloud specific) | `PreferDualStack` |
-| `service.ipFamilies` | Controller Service ipFamilies (optional, cloud specific) | `["IPv6","IPv4"]` |
-| `ingress.enabled` | Enable ingress record generation for codx | `true` |
-| `ingress.pathType` | Ingress path type | `ImplementationSpecific` |
-| `ingress.apiVersion` | Force Ingress API version (automatically detected if not set) | `""` |
-| `ingress.hostname` | Default host for the ingress record | `codx.local` |
-| `ingress.ingressClassName` | IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) | `traefik` |
-| `ingress.ingressControllerType` | ingressControllerType that will be be used to implement the Ingress specific annotations (Ex. nginx or traefik) | `traefik` |
-| `ingress.path` | Default path for the ingress record | `/` |
-| `ingress.annotations` | Additional annotations for the Ingress resource. To enable certificate autogeneration, place here your cert-manager annotations. | `{}` |
-| `ingress.extraMiddlewares` | Extra Traefik middlewares appended to the traefik.ingress.kubernetes.io/router.middlewares annotation, after any user-defined middlewares and the chart passTLSClientCert middleware (when traefikMtls.enabled). Entries are middleware references; the "@kubernetescrd" provider suffix is added when missing. | `[]` |
-| `ingress.tls` | Enable TLS configuration for the host defined at `ingress.hostname` parameter | `false` |
-| `ingress.selfSigned` | Create a TLS secret for this ingress record using self-signed certificates generated by Helm | `false` |
-| `ingress.extraHosts` | An array with additional hostname(s) to be covered with the ingress record | `[]` |
-| `ingress.extraPaths` | An array with additional arbitrary paths that may need to be added to the ingress under the main host | `[]` |
-| `ingress.extraTls` | TLS configuration for additional hostname(s) to be covered with this ingress record | `[]` |
-| `ingress.secrets` | Custom TLS certificates as secrets | `[]` |
-| `ingress.extraRules` | Additional rules to be covered with this ingress record | `[]` |
+| Name | Description | Value |
+| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- |
+| `service.type` | Kubernetes service type | `ClusterIP` |
+| `service.ports.https` | codx service HTTPS port | `8443` |
+| `service.nodePorts` | Specify the nodePort values for the LoadBalancer and NodePort service types. | `{}` |
+| `service.sessionAffinity` | Control where client requests go, to the same pod or round-robin | `None` |
+| `service.sessionAffinityConfig` | Additional settings for the sessionAffinity | `{}` |
+| `service.clusterIP` | codx service clusterIP IP | `""` |
+| `service.loadBalancerIP` | loadBalancerIP for the CodX service (optional, cloud specific) | `""` |
+| `service.loadBalancerSourceRanges` | Address that are allowed when service is LoadBalancer | `[]` |
+| `service.externalTrafficPolicy` | Enable client source IP preservation | `Cluster` |
+| `service.annotations` | Additional custom annotations for codx service | `{}` |
+| `service.extraPorts` | Extra port to expose on codx service | `[]` |
+| `service.extraHeadlessPorts` | Extra ports to expose on codx headless service | `[]` |
+| `service.ipFamilyPolicy` | Controller Service ipFamilyPolicy (optional, cloud specific) | `PreferDualStack` |
+| `service.ipFamilies` | Controller Service ipFamilies (optional, cloud specific) | `["IPv6","IPv4"]` |
+| `ingress.enabled` | Enable IngressRoute record generation for codx | `true` |
+| `ingress.apiVersion` | Force IngressRoute API version: traefik.io/v1alpha1 (Traefik v3, default) or traefik.containo.us/v1alpha1 (Traefik v2) | `traefik.io/v1alpha1` |
+| `ingress.hostname` | Default host for the IngressRoute record | `codx.local` |
+| `ingress.path` | Default path for the IngressRoute record | `/` |
+| `ingress.entryPoints` | Traefik entryPoints the IngressRoute binds to (e.g. websecure). Empty binds to all entry points. | `[]` |
+| `ingress.annotations` | Additional annotations for the IngressRoute resource | `{}` |
+| `ingress.middlewares` | Traefik Middlewares attached to the IngressRoute routes, before the chart passTLSClientCert middleware (when traefikMtls.enabled). A namespace defaults to the release namespace; cross-namespace references require the Traefik allowCrossNamespace option. | `[]` |
+| `ingress.tls.enabled` | Enable TLS on the IngressRoute | `false` |
+| `ingress.tls.secretName` | Name of the Secret holding the TLS certificate (in the release namespace). Optional: when empty, no secretName is set on the IngressRoute (use certResolver or a TLSStore instead). | `""` |
+| `ingress.tls.certResolver` | Traefik certificate resolver (ACME) used to generate the certificate, e.g. letsencrypt | `""` |
+| `ingress.tls.options.name` | Name of the TLSOption applied to the route | `""` |
+| `ingress.tls.options.namespace` | Namespace of the TLSOption. Defaults to the release namespace. | `""` |
+| `ingress.extraHosts` | An array with additional hostname(s) to be covered with the IngressRoute record | `[]` |
+| `ingress.certManager.enabled` | Create a cert-manager Certificate for the IngressRoute frontend TLS (implies ingress.tls.enabled) | `false` |
+| `ingress.certManager.issuerRef.name` | Name of the cert-manager Issuer/ClusterIssuer issuing the ingress certificate (required when enabled) | `""` |
+| `ingress.certManager.issuerRef.kind` | Kind of the issuer resource (Issuer or ClusterIssuer) | `ClusterIssuer` |
+| `ingress.certManager.issuerRef.group` | Group of the issuer resource | `cert-manager.io` |
+| `ingress.certManager.labels` | Additional labels for the Certificate (in addition to commonLabels) | `{}` |
+| `ingress.certManager.annotations` | Additional annotations for the Certificate (in addition to commonAnnotations) | `{}` |
### RBAC parameter
@@ -456,7 +460,7 @@ $ helm delete --purge my-release
| `profiles.default.podSpec.securityContext.runAsUser` | User ID of the code-server container. | `1000` |
| `profiles.default.podSpec.securityContext.runAsGroup` | Group ID of the code-server container. | `1000` |
| `profiles.default.podSpec.securityContext.fsGroup` | Group ID of the volumes mounted by the code-server container. | `1000` |
-| `profiles.default.podSpec.codeServer.image` | code-server image of the default profile. | `docker.io/captnbp/code-server:4.141.0` |
+| `profiles.default.podSpec.codeServer.image` | code-server image of the default profile. | `docker.io/captnbp/code-server:4.144.0` |
| `profiles.default.podSpec.codeServer.resources.limits.cpu` | CPU limit of the code-server container. | `2` |
| `profiles.default.podSpec.codeServer.resources.limits.memory` | Memory limit of the code-server container. | `4Gi` |
| `profiles.default.podSpec.codeServer.resources.requests.cpu` | CPU request of the code-server container. | `250m` |
diff --git a/charts/codx/templates/_helpers.tpl b/charts/codx/templates/_helpers.tpl
index bbd7350..e35644f 100644
--- a/charts/codx/templates/_helpers.tpl
+++ b/charts/codx/templates/_helpers.tpl
@@ -25,16 +25,6 @@ Create the name of the service account to use
{{- end -}}
{{- end -}}
-{{/*
-Return true if cert-manager required annotations for TLS signed certificates are set in the Ingress annotations
-Ref: https://cert-manager.io/docs/usage/ingress/#supported-annotations
-*/}}
-{{- define "codx.ingress.certManagerRequest" -}}
-{{ if or (hasKey . "cert-manager.io/cluster-issuer") (hasKey . "cert-manager.io/issuer") }}
- {{- true -}}
-{{- end -}}
-{{- end -}}
-
{{/*
Return true if a TLS credentials secret object should be created
*/}}
@@ -56,6 +46,14 @@ Return the TLS secret name
{{- end -}}
{{- end -}}
+{{/*
+Ingress frontend TLS Secret name. Defaults to -ingress-tls, the
+Secret the chart Certificate (ingress.certManager.enabled) issues into.
+*/}}
+{{- define "codx.ingressTlsSecretName" -}}
+{{- .Values.ingress.tls.secretName | default (printf "%s-ingress-tls" (include "common.names.fullname" .)) -}}
+{{- end -}}
+
{{/*
Instance name: defaults to release name, used in workspace object names.
*/}}
diff --git a/charts/codx/templates/ingress-certificate.yaml b/charts/codx/templates/ingress-certificate.yaml
new file mode 100644
index 0000000..5973f14
--- /dev/null
+++ b/charts/codx/templates/ingress-certificate.yaml
@@ -0,0 +1,43 @@
+{{/*
+cert-manager Certificate for the IngressRoute frontend TLS (between Traefik
+and the clients). Independent from the chart internal TLS (tls.enabled),
+which covers the service certificate, not the public hostname.
+
+Issued for ingress.hostname (commonName) and every ingress.extraHosts name,
+stored in the Secret the IngressRoute references as tls.secretName (defaults
+to -ingress-tls).
+*/}}
+{{- if and .Values.ingress.enabled .Values.ingress.certManager.enabled }}
+{{- $fullname := include "common.names.fullname" . }}
+{{- $hostname := required "ingress.hostname is required when ingress.certManager.enabled is true" .Values.ingress.hostname }}
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: {{ $fullname }}-ingress
+ namespace: {{ include "common.names.namespace" . | quote }}
+ labels: {{- include "common.labels.standard" . | nindent 4 }}
+ app.kubernetes.io/component: codx
+ {{- if .Values.commonLabels }}
+ {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
+ {{- end }}
+ {{- if .Values.ingress.certManager.labels }}
+ {{- include "common.tplvalues.render" ( dict "value" .Values.ingress.certManager.labels "context" $ ) | nindent 4 }}
+ {{- end }}
+ {{- if or .Values.ingress.certManager.annotations .Values.commonAnnotations }}
+ annotations: {{- include "common.tplvalues.merge" ( dict "values" ( list .Values.ingress.certManager.annotations .Values.commonAnnotations ) "context" $ ) | nindent 4 }}
+ {{- end }}
+spec:
+ secretName: {{ include "codx.ingressTlsSecretName" . }}
+ commonName: {{ $hostname | quote }}
+ dnsNames:
+ - {{ $hostname | quote }}
+ {{- range .Values.ingress.extraHosts }}
+ - {{ .name | quote }}
+ {{- end }}
+ usages:
+ - server auth
+ issuerRef:
+ name: {{ required "ingress.certManager.issuerRef.name is required when ingress.certManager.enabled is true" .Values.ingress.certManager.issuerRef.name }}
+ kind: {{ .Values.ingress.certManager.issuerRef.kind | quote }}
+ group: {{ .Values.ingress.certManager.issuerRef.group | quote }}
+{{- end }}
diff --git a/charts/codx/templates/ingress.yaml b/charts/codx/templates/ingress.yaml
deleted file mode 100644
index 69c25b8..0000000
--- a/charts/codx/templates/ingress.yaml
+++ /dev/null
@@ -1,88 +0,0 @@
-{{- if .Values.ingress.enabled }}
-{{- $mtlsMiddleware := and .Values.traefikMtls.enabled (eq .Values.ingress.ingressControllerType "traefik") }}
-apiVersion: {{ include "common.capabilities.ingress.apiVersion" . }}
-kind: Ingress
-metadata:
- name: {{ template "common.names.fullname" . }}
- namespace: {{ include "common.names.namespace" . | quote }}
- labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
- app.kubernetes.io/component: codx
- {{- $ann := dict }}
- {{- if or .Values.ingress.annotations .Values.commonAnnotations }}
- {{- $ann = include "common.tplvalues.merge" ( dict "values" ( list .Values.ingress.annotations .Values.commonAnnotations ) "context" . ) | fromYaml }}
- {{- end }}
- {{- $mwKey := "traefik.ingress.kubernetes.io/router.middlewares" }}
- {{- if and (eq .Values.ingress.ingressControllerType "traefik") (or $mtlsMiddleware .Values.ingress.extraMiddlewares) }}
- {{- $mwList := list }}
- {{- if index $ann $mwKey }}
- {{- $mwList = append $mwList (index $ann $mwKey) }}
- {{- end }}
- {{- if $mtlsMiddleware }}
- {{- $mwList = append $mwList (printf "%s-%s@kubernetescrd" (include "common.names.namespace" .) (include "codx.traefikMtls.middlewareName" .)) }}
- {{- end }}
- {{- range .Values.ingress.extraMiddlewares }}
- {{- $ref := . }}
- {{- if not (contains "@" $ref) }}
- {{- $ref = printf "%s@kubernetescrd" $ref }}
- {{- end }}
- {{- $mwList = append $mwList $ref }}
- {{- end }}
- {{- $_ := set $ann $mwKey (join "," $mwList) }}
- {{- end }}
- {{- if $mtlsMiddleware }}
- {{- if not (index $ann "traefik.ingress.kubernetes.io/router.tls.options") }}
- {{- $_ := set $ann "traefik.ingress.kubernetes.io/router.tls.options" (printf "%s-%s@kubernetescrd" (include "common.names.namespace" .) (include "codx.traefikMtls.tlsOptionName" .)) }}
- {{- end }}
- {{- end }}
- {{- if $ann }}
- annotations: {{- include "common.tplvalues.render" ( dict "value" $ann "context" $) | nindent 4 }}
- {{- else }}
- annotations: {}
- {{- end }}
-spec:
- {{- if .Values.ingress.ingressClassName }}
- ingressClassName: {{ .Values.ingress.ingressClassName | quote }}
- {{- end }}
- rules:
- {{- if .Values.ingress.hostname }}
- - host: {{ .Values.ingress.hostname }}
- http:
- paths:
- {{- if .Values.ingress.extraPaths }}
- {{- toYaml .Values.ingress.extraPaths | nindent 10 }}
- {{- end }}
- - path: {{ .Values.ingress.path }}
- pathType: {{ .Values.ingress.pathType }}
- {{- if .Values.tls.enabled }}
- backend: {{- include "common.ingress.backend" (dict "serviceName" (include "common.names.fullname" .) "servicePort" "https" "context" $) | nindent 14 }}
- {{- else }}
- backend: {{- include "common.ingress.backend" (dict "serviceName" (include "common.names.fullname" .) "servicePort" "http" "context" $) | nindent 14 }}
- {{- end }}
- {{- end }}
- {{- range .Values.ingress.extraHosts }}
- - host: {{ .name | quote }}
- http:
- paths:
- - path: {{ default "/" .path }}
- pathType: {{ default "ImplementationSpecific" .pathType }}
- {{- if .Values.tls.enabled }}
- backend: {{- include "common.ingress.backend" (dict "serviceName" (include "common.names.fullname" $) "servicePort" "https" "context" $) | nindent 14 }}
- {{- else }}
- backend: {{- include "common.ingress.backend" (dict "serviceName" (include "common.names.fullname" .) "servicePort" "http" "context" $) | nindent 14 }}
- {{- end }}
- {{- end }}
- {{- if .Values.ingress.extraRules }}
- {{- include "common.tplvalues.render" (dict "value" .Values.ingress.extraRules "context" $) | nindent 4 }}
- {{- end }}
- {{- if or (and .Values.ingress.tls (or (include "common.ingress.certManagerRequest" ( dict "annotations" .Values.ingress.annotations )) .Values.ingress.selfSigned)) .Values.ingress.extraTls }}
- tls:
- {{- if and .Values.ingress.tls (or (include "common.ingress.certManagerRequest" ( dict "annotations" .Values.ingress.annotations )) .Values.ingress.selfSigned) }}
- - hosts:
- - {{ .Values.ingress.hostname | quote }}
- secretName: {{ printf "%s-tls" .Values.ingress.hostname }}
- {{- end }}
- {{- if .Values.ingress.extraTls }}
- {{- include "common.tplvalues.render" (dict "value" .Values.ingress.extraTls "context" $) | nindent 4 }}
- {{- end }}
- {{- end }}
-{{- end }}
\ No newline at end of file
diff --git a/charts/codx/templates/ingressroute.yaml b/charts/codx/templates/ingressroute.yaml
new file mode 100644
index 0000000..3387de8
--- /dev/null
+++ b/charts/codx/templates/ingressroute.yaml
@@ -0,0 +1,91 @@
+{{/*
+Traefik IngressRoute exposing the CodX service. Requires the Traefik
+Kubernetes CRD provider (IngressRoute, Middleware, TLSOption).
+
+The backend service only exposes the HTTPS port; when the chart-internal
+TLS is enabled the route targets it with the chart ServersTransport
+(, created in serverstransport.yaml) so Traefik trusts the
+self-signed CodX certificate.
+*/}}
+{{- if .Values.ingress.enabled }}
+{{- $fullname := include "common.names.fullname" . }}
+{{- $mtlsMiddleware := .Values.traefikMtls.enabled }}
+{{- $hosts := list }}
+{{- if .Values.ingress.hostname }}
+{{- $hosts = append $hosts (dict "name" .Values.ingress.hostname "path" .Values.ingress.path) }}
+{{- end }}
+{{- range .Values.ingress.extraHosts }}
+{{- $hosts = append $hosts (dict "name" .name "path" (default "/" .path)) }}
+{{- end }}
+apiVersion: {{ .Values.ingress.apiVersion }}
+kind: IngressRoute
+metadata:
+ name: {{ $fullname }}
+ namespace: {{ include "common.names.namespace" . | quote }}
+ labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }}
+ app.kubernetes.io/component: codx
+ {{- if or .Values.ingress.annotations .Values.commonAnnotations }}
+ annotations: {{- include "common.tplvalues.merge" ( dict "values" ( list .Values.ingress.annotations .Values.commonAnnotations ) "context" . ) | nindent 4 }}
+ {{- end }}
+spec:
+ {{- with .Values.ingress.entryPoints }}
+ entryPoints: {{- toYaml . | nindent 4 }}
+ {{- end }}
+ routes:
+ {{- range $hosts }}
+ - match: {{ printf "Host(`%s`) && PathPrefix(`%s`)" .name .path | quote }}
+ kind: Rule
+ {{- if or $.Values.ingress.middlewares $mtlsMiddleware }}
+ middlewares:
+ {{- range $.Values.ingress.middlewares }}
+ - name: {{ .name }}
+ {{- if .namespace }}
+ namespace: {{ .namespace | quote }}
+ {{- end }}
+ {{- end }}
+ {{- if $mtlsMiddleware }}
+ - name: {{ include "codx.traefikMtls.middlewareName" $ }}
+ {{- end }}
+ {{- end }}
+ services:
+ - name: {{ $fullname }}
+ port: {{ $.Values.containerPorts.https }}
+ {{- if $.Values.tls.enabled }}
+ scheme: https
+ serversTransport: {{ $fullname }}
+ {{- end }}
+ {{- end }}
+ {{/* ingress.certManager.enabled implies the frontend TLS: the route serves
+ the Secret the chart Certificate issues into. */}}
+ {{- if or .Values.ingress.tls.enabled .Values.ingress.certManager.enabled }}
+ tls:
+ {{- if or .Values.ingress.tls.secretName .Values.ingress.certManager.enabled }}
+ secretName: {{ include "codx.ingressTlsSecretName" . | quote }}
+ {{- end }}
+ {{- if .Values.ingress.tls.certResolver }}
+ certResolver: {{ .Values.ingress.tls.certResolver | quote }}
+ {{- end }}
+ {{- $tlsOptionName := .Values.ingress.tls.options.name }}
+ {{- if and $mtlsMiddleware (not $tlsOptionName) }}
+ {{- $tlsOptionName = include "codx.traefikMtls.tlsOptionName" . }}
+ {{- end }}
+ {{- if $tlsOptionName }}
+ options:
+ name: {{ $tlsOptionName }}
+ {{- if .Values.ingress.tls.options.namespace }}
+ namespace: {{ .Values.ingress.tls.options.namespace | quote }}
+ {{- end }}
+ {{- end }}
+ {{- $domains := list }}
+ {{- range $hosts }}
+ {{- $domains = append $domains .name }}
+ {{- end }}
+ {{- if $domains }}
+ domains:
+ - main: {{ index $domains 0 | quote }}
+ {{- if gt (len $domains) 1 }}
+ sans: {{- rest $domains | toYaml | nindent 10 }}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/charts/codx/templates/serverstransport.yaml b/charts/codx/templates/serverstransport.yaml
index d2faed6..d707e73 100644
--- a/charts/codx/templates/serverstransport.yaml
+++ b/charts/codx/templates/serverstransport.yaml
@@ -1,4 +1,4 @@
-{{- if and (eq .Values.ingress.ingressControllerType "traefik") .Values.tls.enabled }}
+{{- if .Values.tls.enabled }}
{{- $releaseNamespace := .Release.Namespace }}
{{- $clusterDomain := .Values.clusterDomain }}
{{- $serviceName := include "common.names.fullname" . }}
diff --git a/charts/codx/templates/service.yaml b/charts/codx/templates/service.yaml
index 31e6de7..4f76c95 100644
--- a/charts/codx/templates/service.yaml
+++ b/charts/codx/templates/service.yaml
@@ -9,10 +9,6 @@ metadata:
{{- if or .Values.service.annotations .Values.commonAnnotations }}
{{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.service.annotations .Values.commonAnnotations ) "context" . ) }}{{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }}
{{- end }}
- {{- if and (eq .Values.ingress.ingressControllerType "traefik") .Values.tls.enabled }}
- traefik.ingress.kubernetes.io/service.serversscheme: https
- traefik.ingress.kubernetes.io/service.serverstransport: "{{ .Release.Namespace }}-{{ template "common.names.fullname" . }}@kubernetescrd"
- {{- end }}
spec:
type: {{ .Values.service.type }}
{{- if and .Values.service.clusterIP (eq .Values.service.type "ClusterIP") }}
diff --git a/charts/codx/templates/traefik-mtls.yaml b/charts/codx/templates/traefik-mtls.yaml
index 351d671..0ab90b8 100644
--- a/charts/codx/templates/traefik-mtls.yaml
+++ b/charts/codx/templates/traefik-mtls.yaml
@@ -9,7 +9,7 @@ Traefik objects for the mTLS admin fallback authentication:
Requires the Traefik CRDs (traefik.io/v1alpha1 Middleware and TLSOption).
All objects are created in the release namespace, the same namespace as the
-Ingress, so Traefik resolves the plain-name references (name@kubernetescrd).
+IngressRoute, so Traefik resolves the plain-name references.
*/}}
{{- if .Values.traefikMtls.enabled }}
{{- $fullname := include "common.names.fullname" . }}
diff --git a/charts/codx/values.yaml b/charts/codx/values.yaml
index c230cf2..46eb042 100644
--- a/charts/codx/values.yaml
+++ b/charts/codx/values.yaml
@@ -315,118 +315,111 @@ service:
## codx ingress parameters
-## ref: http://kubernetes.io/docs/concepts/services-networking/ingress/
+## ref: https://doc.traefik.io/traefik/providers/kubernetes-crd/
+## The chart exposes CodX with a Traefik IngressRoute, which requires the
+## Traefik Kubernetes CRD provider (IngressRoute, Middleware, TLSOption).
+## Set enabled=false to expose CodX with your own objects instead (e.g. a
+## standard Ingress for another controller).
##
ingress:
- ## @param ingress.enabled Enable ingress record generation for codx
+ ## @param ingress.enabled Enable IngressRoute record generation for codx
##
enabled: true
- ## @param ingress.pathType Ingress path type
+ ## @param ingress.apiVersion Force IngressRoute API version: traefik.io/v1alpha1 (Traefik v3, default) or traefik.containo.us/v1alpha1 (Traefik v2)
##
- pathType: ImplementationSpecific
- ## @param ingress.apiVersion Force Ingress API version (automatically detected if not set)
- ##
- apiVersion: ""
- ## @param ingress.hostname Default host for the ingress record
+ apiVersion: traefik.io/v1alpha1
+ ## @param ingress.hostname Default host for the IngressRoute record
##
hostname: codx.local
- ## @param ingress.ingressClassName IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+)
- ## This is supported in Kubernetes 1.18+ and required if you have more than one IngressClass marked as the default for your cluster .
- ## ref: https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/
- ##
- ingressClassName: "traefik"
- ## @param ingress.ingressControllerType ingressControllerType that will be be used to implement the Ingress specific annotations (Ex. nginx or traefik)
- ingressControllerType: "traefik"
- ## @param ingress.path Default path for the ingress record
- ## NOTE: You may need to set this to '/*' in order to use this with ALB ingress controllers
+ ## @param ingress.path Default path for the IngressRoute record
##
path: /
- ## @param ingress.annotations Additional annotations for the Ingress resource. To enable certificate autogeneration, place here your cert-manager annotations.
- ## Use this parameter to set the required annotations for cert-manager, see
- ## ref: https://cert-manager.io/docs/usage/ingress/#supported-annotations
+ ## @param ingress.entryPoints [array] Traefik entryPoints the IngressRoute binds to (e.g. websecure). Empty binds to all entry points.
+ ## e.g:
+ ## entryPoints:
+ ## - websecure
+ ##
+ entryPoints: []
+ ## @param ingress.annotations Additional annotations for the IngressRoute resource
## e.g:
## annotations:
- ## kubernetes.io/ingress.class: nginx
- ## cert-manager.io/cluster-issuer: cluster-issuer-name
+ ## external-dns.alpha.kubernetes.io/hostname: codx.example.com
##
annotations: {}
- ## @param ingress.extraMiddlewares [array] Extra Traefik middlewares appended to the traefik.ingress.kubernetes.io/router.middlewares annotation, after any user-defined middlewares and the chart passTLSClientCert middleware (when traefikMtls.enabled). Entries are middleware references; the "@kubernetescrd" provider suffix is added when missing.
+ ## @param ingress.middlewares [array] Traefik Middlewares attached to the IngressRoute routes, before the chart passTLSClientCert middleware (when traefikMtls.enabled). A namespace defaults to the release namespace; cross-namespace references require the Traefik allowCrossNamespace option.
## e.g:
- ## extraMiddlewares:
- ## - my-middleware
- ## - traefik-my-other-middleware@kubernetescrd
- ##
- extraMiddlewares: []
- ## @param ingress.tls Enable TLS configuration for the host defined at `ingress.hostname` parameter
- ## TLS certificates will be retrieved from a TLS secret with name: `{{- printf "%s-tls" .Values.ingress.hostname }}`
- ## You can:
- ## - Use the `ingress.secrets` parameter to create this TLS secret
- ## - Rely on cert-manager to create it by setting the corresponding annotations
- ## - Rely on Helm to create self-signed certificates by setting `ingress.selfSigned=true`
+ ## middlewares:
+ ## - name: my-middleware
+ ## - name: redirects-to-https
+ ## namespace: traefik
##
- tls: false
- ## @param ingress.selfSigned Create a TLS secret for this ingress record using self-signed certificates generated by Helm
+ middlewares: []
+ ## Frontend TLS of the IngressRoute (between Traefik and the clients). The
+ ## backend TLS to the CodX service is controlled by the `tls` section.
##
- selfSigned: false
- ## @param ingress.extraHosts An array with additional hostname(s) to be covered with the ingress record
+ tls:
+ ## @param ingress.tls.enabled Enable TLS on the IngressRoute
+ ##
+ enabled: false
+ ## @param ingress.tls.secretName Name of the Secret holding the TLS certificate (in the release namespace). Optional: when empty, no secretName is set on the IngressRoute (use certResolver or a TLSStore instead).
+ ##
+ secretName: ""
+ ## @param ingress.tls.certResolver Traefik certificate resolver (ACME) used to generate the certificate, e.g. letsencrypt
+ ##
+ certResolver: ""
+ ## TLSOption applied to the route. Defaults to the chart client-mTLS
+ ## TLSOption when traefikMtls.enabled.
+ ##
+ options:
+ ## @param ingress.tls.options.name Name of the TLSOption applied to the route
+ ##
+ name: ""
+ ## @param ingress.tls.options.namespace Namespace of the TLSOption. Defaults to the release namespace.
+ ##
+ namespace: ""
+ ## @param ingress.extraHosts An array with additional hostname(s) to be covered with the IngressRoute record
## e.g:
## extraHosts:
## - name: codx.local
## path: /
##
extraHosts: []
- ## @param ingress.extraPaths An array with additional arbitrary paths that may need to be added to the ingress under the main host
- ## e.g:
- ## extraPaths:
- ## - path: /*
- ## backend:
- ## serviceName: ssl-redirect
- ## servicePort: use-annotation
+ ## cert-manager Certificate for the IngressRoute frontend TLS. When
+ ## enabled, the chart issues a certificate for ingress.hostname
+ ## (commonName) and every ingress.extraHosts name into the
+ ## -ingress-tls Secret, and the IngressRoute frontend TLS is
+ ## enabled with that Secret as tls.secretName (an explicit
+ ## ingress.tls.secretName wins). The Certificate metadata carries
+ ## commonLabels/commonAnnotations in addition to the dedicated ones below.
##
- extraPaths: []
- ## @param ingress.extraTls TLS configuration for additional hostname(s) to be covered with this ingress record
- ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls
- ## e.g:
- ## extraTls:
- ## - hosts:
- ## - codx.local
- ## secretName: codx.local-tls
- ##
- extraTls: []
- ## @param ingress.secrets Custom TLS certificates as secrets
- ## NOTE: 'key' and 'certificate' are expected in PEM format
- ## NOTE: 'name' should line up with a 'secretName' set further up
- ## If it is not set and you're using cert-manager, this is unneeded, as it will create a secret for you with valid certificates
- ## If it is not set and you're NOT using cert-manager either, self-signed certificates will be created valid for 365 days
- ## It is also possible to create and manage the certificates outside of this helm chart
- ## Please see README.md for more information
- ## e.g:
- ## secrets:
- ## - name: codx.local-tls
- ## key: |-
- ## -----BEGIN RSA PRIVATE KEY-----
- ## ...
- ## -----END RSA PRIVATE KEY-----
- ## certificate: |-
- ## -----BEGIN CERTIFICATE-----
- ## ...
- ## -----END CERTIFICATE-----
- ##
- secrets: []
- ## @param ingress.extraRules Additional rules to be covered with this ingress record
- ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules
- ## e.g:
- ## extraRules:
- ## - host: example.local
- ## http:
- ## path: /
- ## backend:
- ## service:
- ## name: example-svc
- ## port:
- ## name: http
- ##
- extraRules: []
+ certManager:
+ ## @param ingress.certManager.enabled Create a cert-manager Certificate for the IngressRoute frontend TLS (implies ingress.tls.enabled)
+ ##
+ enabled: false
+ ## cert-manager issuer issuing the ingress certificate (e.g. letsencrypt).
+ ##
+ issuerRef:
+ ## @param ingress.certManager.issuerRef.name Name of the cert-manager Issuer/ClusterIssuer issuing the ingress certificate (required when enabled)
+ ##
+ name: ""
+ ## @param ingress.certManager.issuerRef.kind Kind of the issuer resource (Issuer or ClusterIssuer)
+ ##
+ kind: "ClusterIssuer"
+ ## @param ingress.certManager.issuerRef.group Group of the issuer resource
+ ##
+ group: "cert-manager.io"
+ ## @param ingress.certManager.labels Additional labels for the Certificate (in addition to commonLabels)
+ ## e.g:
+ ## labels:
+ ## cert-manager.io/cluster-issuer: letsencrypt
+ ##
+ labels: {}
+ ## @param ingress.certManager.annotations Additional annotations for the Certificate (in addition to commonAnnotations)
+ ## e.g:
+ ## annotations:
+ ## cert-manager.io/issue-priority: "1"
+ ##
+ annotations: {}
## @section RBAC parameter
## Specifies whether a ServiceAccount should be created
@@ -787,11 +780,11 @@ authFallback:
## route, and a passTLSClientCert Middleware forwarding the certificate
## information to CodX in the X-Forwarded-Tls-Client-Cert-Info header.
## Requires the Traefik CRDs (Middleware, TLSOption) and the ingress to be
-## managed by Traefik (ingress.ingressControllerType=traefik). All objects are
-## created in the release namespace. When enabled, the chart also sets the
-## traefik.ingress.kubernetes.io/router.tls.options annotation on the Ingress
-## (unless already set) and appends the Middleware to
-## traefik.ingress.kubernetes.io/router.middlewares.
+## exposed with the chart IngressRoute (ingress.enabled, Traefik CRD
+## provider). All objects are created in the release namespace. When enabled,
+## the chart also appends the Middleware to the IngressRoute routes (after
+## ingress.middlewares) and applies the TLSOption on the IngressRoute
+## (ingress.tls.options, requires ingress.tls.enabled) unless already set.
## @param traefikMtls.enabled Create the client CA Secret, TLSOption and passTLSClientCert Middleware.
## @param traefikMtls.apiVersion API version of the Traefik CRDs: traefik.io/v1alpha1 (Traefik v3, default) or traefik.containo.us/v1alpha1 (Traefik v2).
## @param traefikMtls.existingCaSecret Existing secret with the client CA certificate (key ca.crt), e.g. the chart internal CA "-ca-crt". When empty, a Secret is created from traefikMtls.caCertificates.
@@ -994,7 +987,7 @@ profiles:
runAsGroup: 1000
fsGroup: 1000
codeServer:
- image: docker.io/captnbp/code-server:4.141.0
+ image: docker.io/captnbp/code-server:4.144.0
securityContext:
{}
# allowPrivilegeEscalation: false
diff --git a/docs/README.md b/docs/README.md
index 7531ba3..48fd6e8 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -3,6 +3,9 @@
- [Authentication](authentication.md) - OIDC login flow, sessions, and the
mTLS client-certificate fallback for admins (Traefik TLSOption,
passTLSClientCert Middleware, client CA management).
+- [Ingress exposure](ingress.md) - Traefik IngressRoute, frontend TLS
+ (existing Secret, Traefik ACME resolver or cert-manager certificate),
+ backend ServersTransport and using your own ingress objects.
- [Workspace creation](workspace-creation.md) - per-user Kubernetes objects
(Service, PVC, Certificate, Pod), naming, TLS model and workspace
lifecycle.
diff --git a/docs/authentication.md b/docs/authentication.md
index 81648a1..ce7288f 100644
--- a/docs/authentication.md
+++ b/docs/authentication.md
@@ -8,32 +8,26 @@ mTLS fallback, and the security model.
## Overview
+```mermaid
+flowchart LR
+ Browser["Browser
(no client cert)"]
+ Admin["Admin browser
(client cert)"]
+ Traefik["Traefik ingress
TLSOption (optional)
passTLSClientCert Middleware"]
+ CodX["CodX server
/auth/login, /auth/callback
session middleware"]
+ OIDC["OIDC provider
(e.g. Forgejo)"]
+ Store[("Redis/Valkey
session store")]
+
+ Browser --> Traefik
+ Admin -->|mTLS fallback: admins| Traefik
+ Traefik --> CodX
+ CodX -->|OIDC flow| OIDC
+ CodX --> Store
```
- +-----------------------+ +---------------------+
- Browser | Traefik ingress | | OIDC provider |
- (no client cert) | TLSOption (optional) | | (e.g. Forgejo) |
- -------------| |--------| |
- | | passTLSClientCert | +---------------------+
- | | Middleware |
- | +-----------+-----------+
- | |
- | +-----------v-----------+
- +----------->| CodX server |
- | /auth/login | OIDC flow
- | /auth/callback |
- | session middleware | mTLS fallback (admins)
- +-----------+-----------+
- |
- +-----------v-----------+
- | Redis/Valkey |
- | session store |
- +-----------------------+
-
- Admin browser Traefik validates the client certificate against the
- (client cert) TLSOption CA, forwards the Common Name in the
- X-Forwarded-Tls-Client-Cert-Info header, and CodX grants
- an admin session if the CN is in the allow list.
-```
+
+On the mTLS fallback, Traefik validates the client certificate against the
+TLSOption CA, forwards the Common Name in the
+`X-Forwarded-Tls-Client-Cert-Info` header, and CodX grants an admin session
+if the CN is in the allow list.
Both mechanisms produce the same artifact: a session stored in Redis/Valkey
and referenced by the `codx-session` cookie.
@@ -237,24 +231,25 @@ traefikMtls:
| `TLSOption` | `-client-mtls` | Requests and validates the client certificate on the ingress route (`spec.clientAuth`). |
| `Middleware` | `-passtlsclientcert` | Forwards the certificate CN to CodX in `X-Forwarded-Tls-Client-Cert-Info` (`passTLSClientCert`, `pem: false`). |
-When `traefikMtls.enabled` is true and `ingress.ingressControllerType` is
-`traefik`, the chart also wires the ingress annotations automatically:
-
-- appends `-passtlsclientcert-@kubernetescrd` to
- `traefik.ingress.kubernetes.io/router.middlewares` (your existing list is
- preserved);
-- sets `traefik.ingress.kubernetes.io/router.tls.options` to
- `-client-mtls@kubernetescrd`, unless you set it yourself.
-
-Extra middlewares can be added through the `ingress.extraMiddlewares`
-list: they are appended to `router.middlewares` after any user-defined
-middlewares (from `ingress.annotations`) and the chart
-`passTLSClientCert` middleware. Entries are middleware references; the
-`@kubernetescrd` provider suffix is added when missing.
-
-Both objects are created in the same namespace as the Ingress, so Traefik
-resolves the plain-name references. When enabled, the render fails with an
-explicit error if neither `existingCaSecret` nor `caCertificates` is set.
+When `traefikMtls.enabled` is true, the chart also wires the IngressRoute
+automatically:
+
+- appends the `-passtlsclientcert` Middleware to the route
+ middlewares, after the middlewares from `ingress.middlewares`;
+- applies the `-client-mtls` TLSOption on the IngressRoute
+ (`ingress.tls.options`, requires `ingress.tls.enabled`), unless you set it
+ yourself.
+
+Middlewares can be added through the `ingress.middlewares` list: each entry
+is a middleware `name` with an optional `namespace` (defaults to the
+release namespace; cross-namespace references require the Traefik
+`allowCrossNamespace` option). They run before the chart
+`passTLSClientCert` middleware.
+
+Both objects are created in the same namespace as the IngressRoute, so
+Traefik resolves the plain-name references. When enabled, the render fails
+with an explicit error if neither `existingCaSecret` nor `caCertificates`
+is set.
### Choosing the client CA
diff --git a/docs/inactivity.md b/docs/inactivity.md
index 1512ad6..6fddc77 100644
--- a/docs/inactivity.md
+++ b/docs/inactivity.md
@@ -8,21 +8,20 @@ how to configure the delays.
## Overview
+```mermaid
+flowchart LR
+ CodX["CodX inactivity loops
(activity source, watcher, reconciler)"]
+ API["Kubernetes API"]
+ Envoy["workspace Envoy sidecar
(admin :9901)"]
+ Pod["workspace Pod"]
+
+ CodX -->|lists pods| API
+ CodX -->|poll /stats| Envoy
+ CodX -->|stop: delete| Pod
```
-+--------------+ lists pods +------------------------+
-| CodX |--------------->| Kubernetes API |
-| | +------------------------+
-| inactivity | poll /stats +------------------------+
-| loops |--------------->| workspace Envoy |
-| | | sidecar (admin :9901) |
-| - activity | +------------------------+
-| source |
-| - watcher | stop +------------------------+
-| - reconciler|--------------->| workspace Pod |
-+--------------+ (delete) +------------------------+
- Service, PVC and Certificate are kept: the workspace
- is restarted later with the user's data intact.
-```
+
+Only the Pod is deleted: the Service, PVC and Certificate are kept, so the
+workspace is restarted later with the user's data intact.
Three loops run inside the CodX server, all ticking at the same
`inactivity.checkInterval` (default `60s`):
diff --git a/docs/ingress.md b/docs/ingress.md
new file mode 100644
index 0000000..ef3a1a5
--- /dev/null
+++ b/docs/ingress.md
@@ -0,0 +1,192 @@
+# Ingress exposure (Traefik IngressRoute)
+
+This document describes how the chart exposes the CodX server at the edge:
+the Traefik IngressRoute it creates, the frontend TLS options (including
+the cert-manager certificate), and the backend TLS to the CodX service.
+
+The chart does not create a standard Kubernetes Ingress: CodX is exposed
+through a Traefik IngressRoute, which requires the Traefik Kubernetes CRD
+provider (IngressRoute, Middleware, TLSOption). With another ingress
+controller, set `ingress.enabled=false` and create your own objects (see
+[Using your own ingress objects](#using-your-own-ingress-objects)).
+
+## Overview
+
+```mermaid
+flowchart LR
+ Client["Client (browser)
https://codx.example.com"]
+ Route["IngressRoute <fullname>
match: Host(`codx.example`)
&& PathPrefix(`/`)
frontend TLS: Secret <fullname>-ingress-tls
TLSOption <fullname>-client-mtls (optional)
Middleware <fullname>-passtlsclientcert (optional)"]
+ Service["Service <fullname>:8443
HTTPS only"]
+
+ Client -->|frontend TLS| Route
+ Route -->|"scheme: https
serversTransport: <fullname>"| Service
+
+ style Client fill:#FF6B6B,stroke:#fff,stroke-width:2px,color:#fff
+ style Route fill:#00D9FF,stroke:#fff,stroke-width:2px,color:#000
+ style Service fill:#326CE5,stroke:#fff,stroke-width:2px,color:#fff
+```
+
+The backend Service only exposes the HTTPS port (`containerPorts.https`,
+default 8443): the CodX server serves HTTPS and nothing else (TLS is
+mandatory in the server). Traefik therefore dials the backend with
+`scheme: https` and the chart ServersTransport, which trusts the
+chart-internal CA.
+
+## Created objects
+
+| Object | Name | Created when |
+|--------|------|--------------|
+| IngressRoute | `` | `ingress.enabled` (default) |
+| ServersTransport | `` | `tls.enabled` (default) |
+| Certificate (cert-manager) | `-ingress` | `ingress.certManager.enabled` |
+| TLS Secret (issued by cert-manager) | `-ingress-tls` | by cert-manager, from the Certificate above |
+
+All objects are created in the release namespace.
+
+## Hosts and routes
+
+One route rule is generated per host:
+
+- `ingress.hostname` with `ingress.path` (default `/`),
+- every entry of `ingress.extraHosts` (`name`, `path` defaulting to `/`).
+
+Each rule matches `Host(``) && PathPrefix(``)` and targets the
+`` Service on the HTTPS port. When `ingress.hostname` is empty,
+only the `extraHosts` routes are generated.
+
+The IngressRoute carries `ingress.annotations` (merged with
+`commonAnnotations`) and the standard chart labels plus `commonLabels`.
+Traefik entryPoints are selected with `ingress.entryPoints` (e.g.
+`websecure`); when empty, the route binds to all entry points.
+
+## Middlewares
+
+Middlewares are attached to every route, in order:
+
+1. the ones from `ingress.middlewares` (each entry is `name` + optional
+ `namespace`; a namespace defaults to the release namespace, and
+ cross-namespace references require the Traefik `allowCrossNamespace`
+ option),
+2. the chart `passTLSClientCert` Middleware when `traefikMtls.enabled`.
+
+The mTLS middleware, its TLSOption and the client CA Secret are documented
+in [Authentication](authentication.md).
+
+## Frontend TLS
+
+The `tls` block of the IngressRoute is rendered when `ingress.tls.enabled`
+is true or when `ingress.certManager.enabled` is true (the cert-manager
+certificate implies the frontend TLS - without it, the issued Secret would
+never be served).
+
+| Key | Role |
+|-----|------|
+| `ingress.tls.secretName` | Secret holding the certificate Traefik presents to the clients. Defaults to `-ingress-tls` when `ingress.certManager.enabled` (see below); when empty and no certificate is requested, no `secretName` is set on the route. |
+| `ingress.tls.certResolver` | Alternative to `secretName`: Traefik ACME resolver (e.g. `letsencrypt`) issuing the certificate on the fly. |
+| `ingress.tls.options.name` / `.namespace` | TLSOption applied to the route (e.g. to request client certificates). Defaults to the chart client-mTLS TLSOption when `traefikMtls.enabled` and nothing is set. |
+
+The route also carries the TLS domains, so Traefik answers for every host:
+`main` is the first host (`ingress.hostname` or the first `extraHosts`
+entry), the other hosts are listed as `sans`.
+
+### cert-manager certificate (`ingress.certManager`)
+
+Instead of an existing Secret or a Traefik ACME resolver, the chart can
+request the certificate from cert-manager. With
+`ingress.certManager.enabled=true` the chart creates:
+
+| Property | Value |
+|----------|-------|
+| Certificate name | `-ingress` |
+| Secret | `ingress.tls.secretName` or `-ingress-tls` |
+| commonName | `ingress.hostname` |
+| dnsNames | `ingress.hostname` and every `ingress.extraHosts` name |
+| usages | `server auth` |
+| issuerRef | `ingress.certManager.issuerRef` (`name` required, `kind` default `ClusterIssuer`, `group` default `cert-manager.io`) |
+| labels / annotations | standard chart labels plus `commonLabels` and `ingress.certManager.labels`; `ingress.certManager.annotations` merged with `commonAnnotations` |
+
+Example:
+
+```yaml
+ingress:
+ hostname: codx.example.com
+ extraHosts:
+ - name: www.codx.example.com
+ tls:
+ enabled: true
+ certManager:
+ enabled: true
+ issuerRef:
+ name: letsencrypt
+ kind: ClusterIssuer
+```
+
+Notes:
+
+- `ingress.hostname` and `ingress.certManager.issuerRef.name` are required
+ when the certificate is enabled: `helm template` fails with an explicit
+ error message otherwise.
+- An explicit `ingress.tls.secretName` wins over the
+ `-ingress-tls` default for both the Certificate and the
+ IngressRoute.
+- The ingress certificate is independent from the chart internal TLS
+ (`tls.enabled`), which covers the CodX *service* certificate - not the
+ public hostname.
+- The Certificate requires cert-manager to be installed in the cluster.
+
+## Backend TLS (ServersTransport)
+
+The CodX server presents the certificate issued by the chart internal CA
+(`-server-tls`). For Traefik to trust it when dialing
+`https://:8443`, the chart creates the ServersTransport
+`` whenever `tls.enabled` is true:
+
+- `rootCAs`: the `ca.crt` of the `-server-tls` Secret,
+- `serverName`: the Service FQDN `..svc.`,
+ matching the certificate DNS names.
+
+Since the server speaks HTTPS only, keep `tls.enabled` (the default) when
+using the chart IngressRoute: with it disabled the route dials the backend
+as plain HTTP and the connection fails.
+
+## Traefik v2 and v3
+
+The CRD API version differs between Traefik major versions:
+
+| Traefik | Setting | Value |
+|---------|---------|-------|
+| v3 (default) | `ingress.apiVersion` and `traefikMtls.apiVersion` | `traefik.io/v1alpha1` |
+| v2 | `ingress.apiVersion` and `traefikMtls.apiVersion` | `traefik.containo.us/v1alpha1` |
+
+## Using your own ingress objects
+
+Set `ingress.enabled=false` to skip the IngressRoute and expose CodX with
+your own objects (e.g. a standard Ingress for another controller, or an
+IngressRoute you manage). Requirements to keep in mind:
+
+- route to the Service `` on the HTTPS port `containerPorts.https`
+ (8443) over HTTPS, trusting the chart CA (or replicate the ServersTransport
+ settings),
+- allow the ingress controller traffic in the NetworkPolicy
+ (`networkPolicy.codx.ingress.fromIngressController`, enabled by default),
+- the mTLS fallback objects (`traefikMtls`) still need to be wired to your
+ route by hand: the chart only attaches them to its own IngressRoute.
+
+## Troubleshooting
+
+| Symptom | Likely cause and check |
+|---------|------------------------|
+| 404 on the hostname | Route/host mismatch: check the rendered match rules (`helm template`, look for `Host(\`...\`)`), the `ingress.entryPoints` list and that Traefik serves that entry point. |
+| Traefik serves the wrong / default certificate | No `secretName` on the route: enable `ingress.tls.enabled` or `ingress.certManager.enabled`, or check the Secret exists in the release namespace. |
+| Certificate stays `Not Ready` | cert-manager cannot issue: `kubectl describe certificate -ingress` (issuerRef name/kind, ACME challenge of the `issuerRef` issuer). |
+| 502 / connection reset to the backend | Backend dial failure: keep `tls.enabled=true` (the server only speaks HTTPS) and check the ServersTransport `` exists; check the NetworkPolicy allows the ingress controller. |
+| Client certificates ignored | The client-mTLS TLSOption is not applied: it requires the frontend TLS and defaults to the chart TLSOption only when `traefikMtls.enabled` - see [Authentication](authentication.md). |
+| CRDs rejected by the API server | Wrong CRD API version for your Traefik: set `ingress.apiVersion` (and `traefikMtls.apiVersion`) to `traefik.containo.us/v1alpha1` on Traefik v2. |
+
+## Related documents
+
+- [Authentication](authentication.md) - the mTLS fallback objects
+ (TLSOption, passTLSClientCert Middleware, client CA Secret).
+- [Workspace creation](workspace-creation.md) - the full TLS model, from
+ the browser to the workspace.
+- [Documentation index](README.md)
diff --git a/docs/workspace-creation.md b/docs/workspace-creation.md
index c4413bf..9817de7 100644
--- a/docs/workspace-creation.md
+++ b/docs/workspace-creation.md
@@ -6,25 +6,21 @@ naming, the TLS model, and the lifecycle of a workspace.
## Overview
-```
- User CodX server Kubernetes
- ---- ---------- ----------
- login ------> OIDC session (username -> slug)
- pick a profile
- Start ------> /api/workspace/start?profile= (SSE stream)
- | EnsureWorkspace (idempotent):
- | 1. Service -
- | 2. PVC -
- | 3. cert-manager Certificate
- | 4. Pod -
- | Wait for the certificate to be Ready
- | Wait for the pod to be Ready
- <------ ready redirect to /user//
- |
- browser <-----> CodX proxy /user//... --mTLS--> Envoy sidecar
- (TLS :9443)
- --> code-server
- (localhost:8080)
+```mermaid
+sequenceDiagram
+ participant U as User
+ participant C as CodX server
+ participant K as Kubernetes
+
+ U->>C: login (OIDC session, username mapped to slug)
+ U->>C: pick a profile and Start (SSE stream)
+ C->>K: EnsureWorkspace (idempotent)
+ Note over C,K: 1. Service {instance}-{slug}
2. PVC {instance}-{slug}
3. cert-manager Certificate
4. Pod {instance}-{slug}
+ C->>K: Wait for the certificate to be Ready
+ C->>K: Wait for the pod to be Ready
+ C->>U: ready, redirect to /user/{slug}/
+ U->>C: browser traffic /user/{slug}/...
+ Note over C: mTLS proxy to the Envoy sidecar (TLS :9443),
which forwards to code-server (localhost:8080)
```
The browser never talks to the workspace pod directly: all workspace traffic
diff --git a/go.mod b/go.mod
index 34ef09e..948f12b 100644
--- a/go.mod
+++ b/go.mod
@@ -1,26 +1,26 @@
module github.com/captnbp/CodX
-go 1.27.1
+go 1.27.3
require (
- github.com/cert-manager/cert-manager v1.21.2
+ github.com/cert-manager/cert-manager v1.21.3
github.com/coreos/go-oidc/v3 v3.21.0
- github.com/go-jose/go-jose/v4 v4.1.4
+ github.com/go-jose/go-jose/v4 v4.1.7
github.com/go-logr/logr v1.4.4
github.com/go-logr/zerologr v1.2.3
github.com/google/go-cmp v0.7.0
- github.com/prometheus/client_golang v1.24.1
- github.com/redis/go-redis/v9 v9.22.0
+ github.com/prometheus/client_golang v1.25.0
+ github.com/redis/go-redis/v9 v9.23.0
github.com/rs/zerolog v1.35.1
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0
- go.opentelemetry.io/otel v1.46.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0
- go.opentelemetry.io/otel/sdk v1.46.0
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0
+ go.opentelemetry.io/otel v1.47.0
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0
+ go.opentelemetry.io/otel/sdk v1.47.0
golang.org/x/oauth2 v0.37.0
gopkg.in/yaml.v3 v3.0.1
- k8s.io/api v0.37.0
- k8s.io/apimachinery v0.37.0
- k8s.io/client-go v0.37.0
+ k8s.io/api v0.37.2
+ k8s.io/apimachinery v0.37.2
+ k8s.io/client-go v0.37.2
)
require (
@@ -30,61 +30,63 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/felixge/httpsnoop v1.1.0 // indirect
- github.com/fxamacker/cbor/v2 v2.9.1 // indirect
+ github.com/fxamacker/cbor/v2 v2.10.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
- github.com/go-openapi/jsonpointer v1.0.0 // indirect
- github.com/go-openapi/jsonreference v1.0.0 // indirect
- github.com/go-openapi/swag v0.28.0 // indirect
- github.com/go-openapi/swag/cmdutils v0.28.0 // indirect
- github.com/go-openapi/swag/conv v0.28.0 // indirect
- github.com/go-openapi/swag/fileutils v0.28.0 // indirect
- github.com/go-openapi/swag/jsonutils v0.28.0 // indirect
- github.com/go-openapi/swag/loading v0.28.0 // indirect
- github.com/go-openapi/swag/mangling v0.28.0 // indirect
- github.com/go-openapi/swag/netutils v0.28.0 // indirect
- github.com/go-openapi/swag/pools v0.28.0 // indirect
- github.com/go-openapi/swag/stringutils v0.28.0 // indirect
- github.com/go-openapi/swag/typeutils v0.28.0 // indirect
- github.com/go-openapi/swag/yamlutils v0.28.0 // indirect
+ github.com/go-openapi/jsonpointer v1.0.2 // indirect
+ github.com/go-openapi/jsonreference v1.0.3 // indirect
+ github.com/go-openapi/swag v0.29.2 // indirect
+ github.com/go-openapi/swag/cmdutils v0.29.2 // indirect
+ github.com/go-openapi/swag/conv v0.29.2 // indirect
+ github.com/go-openapi/swag/fileutils v0.29.2 // indirect
+ github.com/go-openapi/swag/jsonutils v0.29.2 // indirect
+ github.com/go-openapi/swag/loading v0.29.2 // indirect
+ github.com/go-openapi/swag/mangling v0.29.2 // indirect
+ github.com/go-openapi/swag/netutils v0.29.2 // indirect
+ github.com/go-openapi/swag/pools v0.29.2 // indirect
+ github.com/go-openapi/swag/stringutils v0.29.2 // indirect
+ github.com/go-openapi/swag/typeutils v0.29.2 // indirect
+ github.com/go-openapi/swag/yamlutils v0.29.2 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.33.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/mattn/go-colorable v0.1.14 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-colorable v0.1.16 // indirect
+ github.com/mattn/go-isatty v0.0.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+ github.com/pandatix/go-abnf v0.5.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- github.com/prometheus/client_model v0.6.2 // indirect
- github.com/prometheus/common v0.70.1 // indirect
- github.com/prometheus/procfs v0.21.1 // indirect
+ github.com/prometheus/client_model v0.6.3 // indirect
+ github.com/prometheus/common v0.73.1 // indirect
+ github.com/prometheus/procfs v0.23.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
- go.opentelemetry.io/otel/metric v1.46.0 // indirect
- go.opentelemetry.io/otel/trace v1.46.0 // indirect
- go.opentelemetry.io/proto/otlp v1.11.0 // indirect
- go.uber.org/atomic v1.11.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 // indirect
+ go.opentelemetry.io/otel/log v1.47.0 // indirect
+ go.opentelemetry.io/otel/metric v1.47.0 // indirect
+ go.opentelemetry.io/otel/trace v1.47.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.11.1 // indirect
+ go.uber.org/atomic v1.12.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
- go.yaml.in/yaml/v3 v3.0.5 // indirect
- golang.org/x/net v0.60.0 // indirect
- golang.org/x/sys v0.48.0 // indirect
- golang.org/x/term v0.46.0 // indirect
- golang.org/x/text v0.42.0 // indirect
- golang.org/x/time v0.15.0 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect
- google.golang.org/grpc v1.83.2 // indirect
+ go.yaml.in/yaml/v3 v3.0.6 // indirect
+ golang.org/x/net v0.62.0 // indirect
+ golang.org/x/sys v0.49.0 // indirect
+ golang.org/x/term v0.47.0 // indirect
+ golang.org/x/text v0.43.0 // indirect
+ golang.org/x/time v0.16.0 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20261026165753-2ffab32bce7e // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20261026165753-2ffab32bce7e // indirect
+ google.golang.org/grpc v1.85.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
- k8s.io/apiextensions-apiserver v0.37.0 // indirect
+ k8s.io/apiextensions-apiserver v0.37.2 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
- k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
- sigs.k8s.io/gateway-api v1.6.0 // indirect
- sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
+ k8s.io/utils v0.0.0-20261023123148-ca04ba58ecab // indirect
+ sigs.k8s.io/gateway-api v1.7.0 // indirect
+ sigs.k8s.io/json v0.0.0-20260909141634-11ed52e25bc5 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index 92e1f61..a2b0193 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
-github.com/cert-manager/cert-manager v1.21.2 h1:UWgoYM+KLNtRlqmoWOKjKBOBUPVHeepQ6/ac3ERJW68=
-github.com/cert-manager/cert-manager v1.21.2/go.mod h1:cUXqp2Ki46IovJB+XpkfaFNzitQDJjyfjrCAsl5Jnns=
+github.com/cert-manager/cert-manager v1.21.3 h1:s5xNJPfWSiBWLAxMmXw8runfcXMVVWM4kcMpZ1OODbY=
+github.com/cert-manager/cert-manager v1.21.3/go.mod h1:Y0beWPQTPpBNIdxwDX2EAL24ONep1H4jGOmSKPvoa0I=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-oidc/v3 v3.21.0 h1:wZo4Q9Pum8dYEj0eMUPrqR+kvuGkeUplbLpNCkBqoWM=
@@ -20,10 +20,10 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc=
github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE=
-github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
-github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
-github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
-github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/fxamacker/cbor/v2 v2.10.0 h1:HVF3T5vYwLqO81ebPirWEPNv2SMrUMD3HLnKgEr6uWE=
+github.com/fxamacker/cbor/v2 v2.10.0/go.mod h1:uSwlToFuT9BZAKRxxVm3qQC8DR4tlWl7D6pf20iRYH4=
+github.com/go-jose/go-jose/v4 v4.1.7 h1:Nac/98uT5LPkfbiFuYa8ktf62a2VxwQ4xbq1GK8pV5E=
+github.com/go-jose/go-jose/v4 v4.1.7/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -31,40 +31,40 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs=
github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho=
-github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
-github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
-github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
-github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
-github.com/go-openapi/swag v0.28.0 h1:xkgbOSKj6DZziNpyqRRAOt3GJGtgjgsd2RoyT30VWuw=
-github.com/go-openapi/swag v0.28.0/go.mod h1:4qYnT3Cqr1p1VknOdPo70evN4rgQnAg6jwApHyxSGIg=
-github.com/go-openapi/swag/cmdutils v0.28.0 h1:7TOeNtkYru1SG8Y34tDh9WBbLsMqGnptuxWiHREPZ4Q=
-github.com/go-openapi/swag/cmdutils v0.28.0/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
-github.com/go-openapi/swag/conv v0.28.0 h1:GtqqbyFe7vR5Y7ehxG9W6/OvrSFdf1OLeTGp40TqxH8=
-github.com/go-openapi/swag/conv v0.28.0/go.mod h1:mbUE+mzctnhxi864m0Q07SpN8OowD9JhxmxuYvZZD/k=
-github.com/go-openapi/swag/fileutils v0.28.0 h1:Z04XWQD7R8Eq+7GnOrjovBxPPmZzsS4gt2H2GPGIViU=
-github.com/go-openapi/swag/fileutils v0.28.0/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
-github.com/go-openapi/swag/jsonutils v0.28.0 h1:YIch6FwO7RXzeAnbO8Tu7dWBZeUEH+4nA0HXltVTnv4=
-github.com/go-openapi/swag/jsonutils v0.28.0/go.mod h1:CYM3WlTUcagR2ZoHdz54di/cbBqt82tuxuXgAjxw+mg=
-github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0 h1:qV+VVUAx5Oro8WjVWpZeql7YReTKhT4smR4zhcOQZr0=
-github.com/go-openapi/swag/jsonutils/fixtures_test v0.28.0/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
-github.com/go-openapi/swag/loading v0.28.0 h1:td8QZdZC9MIYGGSnSPKShKiK22I2tU5UQvuUhIBPRLU=
-github.com/go-openapi/swag/loading v0.28.0/go.mod h1:rXB0QiQX5mMveXEA7ouM4KiiM9jVJe4K6BVbwhD1M4k=
-github.com/go-openapi/swag/mangling v0.28.0 h1:pH8eyeNO9SLYsTMWJrurnNfKmDa28XrlA+HePVD53VM=
-github.com/go-openapi/swag/mangling v0.28.0/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
-github.com/go-openapi/swag/netutils v0.28.0 h1:YXN6TALEi2pzts8/8GNm6T61HTAZsieukGZidap989k=
-github.com/go-openapi/swag/netutils v0.28.0/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
-github.com/go-openapi/swag/pools v0.28.0 h1:HPMZWSAfce3rdVTFcjFiCIBtDg9h4x2QlRrHipwhxeU=
-github.com/go-openapi/swag/pools v0.28.0/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
-github.com/go-openapi/swag/stringutils v0.28.0 h1:ixsc9iYgDPubHL/8nSkbnryEHpD2VRlBMLKpQyPXcDU=
-github.com/go-openapi/swag/stringutils v0.28.0/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
-github.com/go-openapi/swag/typeutils v0.28.0 h1:nRBKSBXjDgf01VDPB3fWeD9nQuhCOVeIYAkUx2tbkyY=
-github.com/go-openapi/swag/typeutils v0.28.0/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
-github.com/go-openapi/swag/yamlutils v0.28.0 h1:TV3JXH6DS46KUroDtMLAYHGkdWf5VDq3wVWFirmzROY=
-github.com/go-openapi/swag/yamlutils v0.28.0/go.mod h1:x0q/yndZHEgk9Rx3DyDqzFUmHy55KTvIZldvF2dTJXs=
-github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
-github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
-github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
-github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
+github.com/go-openapi/jsonpointer v1.0.2 h1:NbW7xT2b+l7x+JefiPGDsZWsrLk1ogrz4AHK1TTzMt8=
+github.com/go-openapi/jsonpointer v1.0.2/go.mod h1:N0rI4MbNKD7xAFTiHtB+5Nhrg2j/wwYg7m7Fmb4/jk8=
+github.com/go-openapi/jsonreference v1.0.3 h1:5rDbq+JsjCv6/nUDDJt83nLZoADI4w03uvHXer+4aYQ=
+github.com/go-openapi/jsonreference v1.0.3/go.mod h1:OJSlMXHysgH272D9z2tiy2MGe5A9vQLHaCPb6KbYdE4=
+github.com/go-openapi/swag v0.29.2 h1:9n8frkcsuQRA0INU31VbxNxt1zBaHxrHYGEf5BUjFi4=
+github.com/go-openapi/swag v0.29.2/go.mod h1:RkCiX1gCVXWgLOavDXE3ALJrQ/VO2ziiIzOUs3z10xo=
+github.com/go-openapi/swag/cmdutils v0.29.2 h1:cXEzX/nWCODon251f+1HgL75/R4XXML5IZxgLCWMoro=
+github.com/go-openapi/swag/cmdutils v0.29.2/go.mod h1:XziaSVzYqkDvpWXzASfWajw2YvJrLscq610O2XiyqJw=
+github.com/go-openapi/swag/conv v0.29.2 h1:8c9shoB8l0QRSR6ymq1llHdBWDqpIgJfjTGHx3Vuhm0=
+github.com/go-openapi/swag/conv v0.29.2/go.mod h1:AZS0YigTNf8qNtD6WqJz/N4RUNmpr76SyjFGkq4+p6Q=
+github.com/go-openapi/swag/fileutils v0.29.2 h1:mdUL+Vw5ah1fO1AvFCoHeIyv7YZsCz2MN3KNqd/lLVI=
+github.com/go-openapi/swag/fileutils v0.29.2/go.mod h1:7QKmmodjAebaq61lGVJa8ldLdGeMF4DSgYv8tXOGGo0=
+github.com/go-openapi/swag/jsonutils v0.29.2 h1:uZNSD2/rJDYAfvsLYkykTzXuyxM3QpDKV9jhRHrMu6k=
+github.com/go-openapi/swag/jsonutils v0.29.2/go.mod h1:ONTdNvj3Y+IXRzfa17E+Rgt2ns/qiSOYjIo2K7v2yDs=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2 h1:w+Fd6EBOMGlC74GYGHqCLGyb3Bpams8TtNrgM/SG4jo=
+github.com/go-openapi/swag/jsonutils/fixtures_test v0.29.2/go.mod h1:HC2egPORFzbj/gf05Zrfgz8Mgplx3oW7hgGi7pjZVhM=
+github.com/go-openapi/swag/loading v0.29.2 h1:QU1ry24e6r6Shoxe380Nx0X5iKkpRO+hJffsyJ2dVlY=
+github.com/go-openapi/swag/loading v0.29.2/go.mod h1:DqilDjuiJKETecsS4TRopWG1acUDsfqN39ZmQhPu6uI=
+github.com/go-openapi/swag/mangling v0.29.2 h1:ltdo3K0tTP4P6zWhVVWDU9D37/YYBuMJRdZxiS2Dtog=
+github.com/go-openapi/swag/mangling v0.29.2/go.mod h1:RjDi4TnItAenS3cgCXSpIl1kKbjG7il6pnmQVl/8bp8=
+github.com/go-openapi/swag/netutils v0.29.2 h1:dBli6jyUa93sDS/XYWShJzf4+nnVfFKfvY9SLUaUzB8=
+github.com/go-openapi/swag/netutils v0.29.2/go.mod h1:1pM6Xg/Um8E1eps3umWl8Hme3ByWXOtHsvFC0CULHUE=
+github.com/go-openapi/swag/pools v0.29.2 h1:PlGoDRF8WtSyXkedZZkpW3f9wDhFf3u8KtaBcmgmh8c=
+github.com/go-openapi/swag/pools v0.29.2/go.mod h1:V3lhxVT4qDYRqc2MRSSbLsTYIYV/2HlHafhyEkmzLIc=
+github.com/go-openapi/swag/stringutils v0.29.2 h1:lcnBxwAaysT3bMUVBfn9V/PkfVqkurpqufKU6rTUMGk=
+github.com/go-openapi/swag/stringutils v0.29.2/go.mod h1:i9cdh7sGaa0nm3CqIvH5IM5h2QClk1wns2ktKeILvRI=
+github.com/go-openapi/swag/typeutils v0.29.2 h1:O7aVvkTs3pXwikgtrPLenigEMdQFgONKRooQA9pgf2I=
+github.com/go-openapi/swag/typeutils v0.29.2/go.mod h1:7+GDG+uz9Ke+eVt4rClZg7wklSDp8hT/mKNh/pC26TE=
+github.com/go-openapi/swag/yamlutils v0.29.2 h1:IFKFFeDnuIwzfsuWRQU+rI8iL3g4XyAYjV/RlnlxPLM=
+github.com/go-openapi/swag/yamlutils v0.29.2/go.mod h1:7MGqtcrK73sxQ4ceiyIf8qiXS/s09JLMdR5esK5euYQ=
+github.com/go-openapi/testify/enable/yaml/v2 v2.7.0 h1:wPW6YRgx3+SID1yUy/Xwa17L8kFEaEKod2VRbJDZNUs=
+github.com/go-openapi/testify/enable/yaml/v2 v2.7.0/go.mod h1:mI1M88etYbc3PhgHsWQK2kwvNwW5aGFqMPbmib+SGIs=
+github.com/go-openapi/testify/v2 v2.8.0 h1:19QDx5b57p8KjO2E4sgs/woL4Akp+FGLRlVm2xn0Rho=
+github.com/go-openapi/testify/v2 v2.8.0/go.mod h1:4gUN8jC+RqOE2qjW/vDmfyTvl5BCht4qL7oca0odrak=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
@@ -74,24 +74,24 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.33.0 h1:5T5Ns3kMpWr+z3s5ZA0e7ljVyWNVNxK76sHPAxi5D1k=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.33.0/go.mod h1:oWNYw09cH8wl8fFgC5hKlBLxR4XfKy2KQpP8MJjeCcc=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
-github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
-github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
-github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
-github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
+github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
+github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
+github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
-github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-colorable v0.1.16 h1:+2YjeAe7xP1OQbZW1jERPWWCDLlNqFNCvjiqGiAt84A=
+github.com/mattn/go-colorable v0.1.16/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
+github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -100,19 +100,21 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
+github.com/pandatix/go-abnf v0.5.5 h1:EXex5KkRQx4Dm6GnpAcQnIMBs9KiK0uf8p/wq+4t9PM=
+github.com/pandatix/go-abnf v0.5.5/go.mod h1:AE0KD3/VNoCjGzc1gqetRKshbI4SYErXQ/8kPenR5mc=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU=
-github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE=
-github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
-github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY=
-github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc=
-github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
-github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
-github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq2F0=
-github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
+github.com/prometheus/client_golang v1.25.0 h1:cDtW1TcFsMi1RJc8khwCKB4VhzwV04aKqbYtNOXuiK0=
+github.com/prometheus/client_golang v1.25.0/go.mod h1:j2X/4/kaQHLPnmyEU8zjmkndo1T317VpCIQEml8LTKk=
+github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo=
+github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM=
+github.com/prometheus/common v0.73.1 h1:SZWh/j4AvhCdhX8jvYSnxvPxhWlC9QZkjJ+tOrEr7kU=
+github.com/prometheus/common v0.73.1/go.mod h1:I9aA/Ukz9s1olOobh8KtCkU7B1mQPQsB9UvQPsUQ1Dk=
+github.com/prometheus/procfs v0.23.0 h1:ad+S6FTkzwaMgacKLM5h6bNKExS+DMH0YiUwXItH/o4=
+github.com/prometheus/procfs v0.23.0/go.mod h1:3AVXO0FqLafyYFU/V/IQRZlC8Rgqz8Dm/x6pKLK+RIk=
+github.com/redis/go-redis/v9 v9.23.0 h1:/TTVdJa8BRNsybwwGGnmiYktHNlUXtbOQxlWpHBPL3s=
+github.com/redis/go-redis/v9 v9.23.0/go.mod h1:EWP2UUk+XDaI1s5nQB5mablZN6vccXlmBQTyfnF87D0=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
@@ -131,53 +133,54 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 h1:3g7B90UzBltIDKq1/5mrTGxTnOFDV0ICOhLoxiZ8jlg=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0/go.mod h1:Ef8SuTh59BT7+ofpDxN9z+yOlc4t2GjLmKDgYNJL/NU=
-go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc=
-go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 h1:KrC1YrQeSt46ITMWAbgQx1M1eV1/1TKzttrBzymPmss=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0/go.mod h1:zDSEzoEqsOrgBeGvH66KRgxh90VonFyJqBHA0Pk3+rM=
-go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8=
-go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o=
-go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI=
-go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM=
-go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3kLgPCwSnSE=
-go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
-go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
-go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
-go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
-go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
-go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
-go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0 h1:LxwW/9ctSCv+QkE/cLR7M91ZIkXNMqJtEMi1vCw9U8s=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.72.0/go.mod h1:tOsftB4SslBwwErVEPaenU2RpThXWPIU8DoJHEC4dyw=
+go.opentelemetry.io/otel v1.47.0 h1:j7ALJ/zgkS7Z6aeJW09p8VC9804bC+PpeTfCD4XPnOM=
+go.opentelemetry.io/otel v1.47.0/go.mod h1:8wS9O2qfXrYrzp6hIF/HOYJJf/wIhFPhR2xLuP+iXQU=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0 h1:julhjPeUH/q/7hinbSdDdqt5h7Zw9YWmRlWRhI0jd54=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.47.0/go.mod h1:Ao2mz688LH/tFf0yMAenidq6k2YNSx6SIY2q6jDACck=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0 h1:aXZXsZ012wOgVkqaiQv+Z/d8V0xKjmg70F6m4CE94lc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.47.0/go.mod h1:ziu1gUIJhAaxM3EV1e3Ralk5AAyL1UOYz3lJJ4VSI38=
+go.opentelemetry.io/otel/log v1.47.0 h1:cOTS1CcLbSQeZKanGJ+0JpF/+t4PELi3O3bbl2lqCcI=
+go.opentelemetry.io/otel/log v1.47.0/go.mod h1:9byitSQ5pLC6PpqwGXjqdMKya6ZTswHRZh2vvXT33nw=
+go.opentelemetry.io/otel/metric v1.47.0 h1:4PptaldXx3Eat1XjMZ68pPJEs5wrhlemctZE9a3UdWY=
+go.opentelemetry.io/otel/metric v1.47.0/go.mod h1:ADGSXxRrXM6bjbvLo535EstVFlPpPYZm4LBKixjDHwU=
+go.opentelemetry.io/otel/sdk v1.47.0 h1:zWXEr4j2lFefG87TU6Yg8a7ngfohIKFZHKp0Hf5hC6I=
+go.opentelemetry.io/otel/sdk v1.47.0/go.mod h1:VUc24kiOeoGsxG8G9ULx3fWKvB7jMhnGE8Oi607lgR0=
+go.opentelemetry.io/otel/sdk/metric v1.47.0 h1:lfISg2j93VT6yqdk9OfUaZmw/GfcZqCCV3jdXtsPnKw=
+go.opentelemetry.io/otel/sdk/metric v1.47.0/go.mod h1:ypLp+mW1Nt2x+Szt3b5/i1syodyts49lMOwxpDI3VGw=
+go.opentelemetry.io/otel/trace v1.47.0 h1:JOjX/Oci8K94QHddo+bbfya/Ai/nf6/dt9ZfrFNWSrM=
+go.opentelemetry.io/otel/trace v1.47.0/go.mod h1:jNaSLa2PZEYFG6fRjJABAu+bw4FS08uDmPg28lTghu0=
+go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw=
+go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg=
+go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE=
+go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
-go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
-go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
-golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
-golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
+go.yaml.in/yaml/v3 v3.0.6 h1:zcDfvjXEqTQmpjniUBLc91xYJMB2c4QOFxEF/LCEckM=
+go.yaml.in/yaml/v3 v3.0.6/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
+golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
+golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
-golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
-golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
-golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
-golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
-golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
-golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
-golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
+golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
+golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
+golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
+golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
+golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
+golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
+golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
+golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688 h1:ax2KzoSRIZU/M0cIxri3pKxy99vniH1PVxWC6si/eZI=
-google.golang.org/genproto/googleapis/api v0.0.0-20260819154853-08b0e4226688/go.mod h1:1RJ9BQGyNdZwkGc1eTqkErfRZ6RJyYPHZo73BZ1vQqI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 h1:cYNAzI2sUwhmCcoj9TxvihSrqsxt6uIkj3rDRhSDmW4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
-google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
-google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
+google.golang.org/genproto/googleapis/api v0.0.0-20261026165753-2ffab32bce7e h1:e789sLZjeTdc02ZCQGTSzHOu0fR73+dwPQVKUp67Z88=
+google.golang.org/genproto/googleapis/api v0.0.0-20261026165753-2ffab32bce7e/go.mod h1:vHV3wJM9WZyfV7GyB+OQGq5dWVrFwCDxapPK4WlRD88=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20261026165753-2ffab32bce7e h1:w3fy/G8OrJHYexn+u5y7hFyc8YRRIr7DCevtIOwAcGY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20261026165753-2ffab32bce7e/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
+google.golang.org/grpc v1.85.0 h1:BPBCfuVRQ1qvBhKDOq9uvQllMvO59rKsYhXpp7Dd8vw=
+google.golang.org/grpc v1.85.0/go.mod h1:0rQrOD58yjal/0EaVbGBANindixsCZNQPDV3VKtaKuQ=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -189,24 +192,24 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-k8s.io/api v0.37.0 h1:Z//Vj9N7RA/yS2sDmxyeo7h+RR4zbUrd2vrd3Z0TbB4=
-k8s.io/api v0.37.0/go.mod h1:LKXgcJWMc+f4OLbP5SFR8rulEg07zZhpi/zMULiBImk=
-k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
-k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
-k8s.io/apimachinery v0.37.0 h1:Np2AbDtf8x6RDHiD8T9LbKJ9gaegeVNa8yNm5FuGKm0=
-k8s.io/apimachinery v0.37.0/go.mod h1:RN3nhprFSCxOi5Selxd7oMTXOe/c+ZbcE7Im+TS2zkE=
-k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ=
-k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0=
+k8s.io/api v0.37.2 h1:PnxqnUFy93jr2GcpWDkKOm8wfy25kT9wa572MuQS8oc=
+k8s.io/api v0.37.2/go.mod h1:+NVRLft99kx+Jg9v34GmHw867Dapim89TSyCsn2hK+M=
+k8s.io/apiextensions-apiserver v0.37.2 h1:myaUiAlm1NRLYdpAvWYTqdgeiCepFupJiRFVVueMUYw=
+k8s.io/apiextensions-apiserver v0.37.2/go.mod h1:BFl1aPC89HsgFgYI+3xpFtckvkAeenDTZP84njA9yw4=
+k8s.io/apimachinery v0.37.2 h1:G4t9RGoPDbQML38hz/kQQiltttgd3egg2ODCR8aIc2o=
+k8s.io/apimachinery v0.37.2/go.mod h1:qaAvSNvgU2x07wRM95MSHSJTPQ/ob/LeNAGbeDgqQJg=
+k8s.io/client-go v0.37.2 h1:sgFtyRZukGkOrsu+QgS7LYnbFFJ5ut3MchdBNduD//E=
+k8s.io/client-go v0.37.2/go.mod h1:bo3LFC8YbCtKFPX21Yj8puMsoJLAv/+SvuOODma0s54=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
-k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
-k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
-sigs.k8s.io/gateway-api v1.6.0 h1:735YBRj5NXFrOGX0GoSjwzUIzbz8kiEOfADsqHFmHgE=
-sigs.k8s.io/gateway-api v1.6.0/go.mod h1:FVfx3t389ybeXOqvDghLbdvJdSCfI/PReqCUI3lu3mY=
-sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
-sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
+k8s.io/utils v0.0.0-20261023123148-ca04ba58ecab h1:pjQrNLaF//ytZtWwCAHtW+pqwSKjtWDqzA8+TCX37RI=
+k8s.io/utils v0.0.0-20261023123148-ca04ba58ecab/go.mod h1:DdQ0BJxlQtuMlKDqdg7X5IMMQCXIk2b3d1/4M9OhUYo=
+sigs.k8s.io/gateway-api v1.7.0 h1:duVnm7nO6oXfMMUWfcDoBph47CYR/ME3NGxccOVglaY=
+sigs.k8s.io/gateway-api v1.7.0/go.mod h1:ihME72ink97/HnDMFNpNf9bH/mAcdTAVw0GR35iiZvA=
+sigs.k8s.io/json v0.0.0-20260909141634-11ed52e25bc5 h1:k9jo7ED/fsfOcz3L1P20P5tjTNtsdQNwMOG2aKzHrl8=
+sigs.k8s.io/json v0.0.0-20260909141634-11ed52e25bc5/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
diff --git a/hack/check_mermaid.mjs b/hack/check_mermaid.mjs
new file mode 100644
index 0000000..14ca348
--- /dev/null
+++ b/hack/check_mermaid.mjs
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/*
+Validate the syntax of every mermaid fenced block in the Markdown files of
+the repository with the real Mermaid parser (mermaid.parse) - syntax only,
+no rendering, so no browser or Chromium is needed. jsdom provides the DOM
+globals the Mermaid bundle expects at import time.
+
+Usage:
+ make check-mermaid (installs mermaid + jsdom in a temp dir)
+ node hack/check_mermaid.mjs [FILE...] (mermaid and jsdom must already be
+ resolvable, e.g. via NODE_PATH)
+
+Requires Node.js >= 20.11 (import.meta.dirname).
+Exit code 0 when every block parses, 1 otherwise.
+*/
+
+import { createRequire } from 'node:module';
+import { readdirSync, readFileSync, statSync } from 'node:fs';
+import { join, relative } from 'node:path';
+import { pathToFileURL } from 'node:url';
+
+const MERMAID_BLOCK = /```mermaid\n(.*?)```/gs;
+const EXCLUDED_DIRS = new Set(['.git', 'node_modules']);
+
+function markdownFiles(dir) {
+ const files = [];
+ for (const entry of readdirSync(dir)) {
+ const full = join(dir, entry);
+ if (statSync(full).isDirectory()) {
+ if (EXCLUDED_DIRS.has(entry)) continue;
+ files.push(...markdownFiles(full));
+ } else if (entry.endsWith('.md')) {
+ files.push(full);
+ }
+ }
+ return files;
+}
+
+async function main() {
+ const root = join(import.meta.dirname, '..');
+ const targets = process.argv.length > 2
+ ? process.argv.slice(2)
+ : markdownFiles(root);
+ const blocks = [];
+ for (const file of targets) {
+ const text = readFileSync(file, 'utf8');
+ const display = file.startsWith(root + '/') ? relative(root, file) : file;
+ for (const match of text.matchAll(MERMAID_BLOCK)) {
+ const line = text.slice(0, match.index).split('\n').length;
+ blocks.push({ file: display, line, diagram: match[1] });
+ }
+ }
+ if (blocks.length === 0) {
+ console.log('No mermaid blocks found.');
+ return;
+ }
+
+ // Mermaid's UMD bundle reads DOM globals at import time: set them up
+ // before requiring it. The probe path keeps the resolution anchored to
+ // this script while NODE_PATH supplies the actual packages.
+ const require = createRequire(pathToFileURL(join(import.meta.dirname, 'node_modules/.probe')));
+ const { JSDOM } = require('jsdom');
+ const dom = new JSDOM('');
+ globalThis.window = dom.window;
+ globalThis.document = dom.window.document;
+ Object.defineProperty(globalThis, 'navigator', {
+ value: dom.window.navigator,
+ configurable: true,
+ });
+ const mermaid = require('mermaid').default;
+ mermaid.initialize({ startOnLoad: false });
+
+ let failures = 0;
+ for (const { file, line, diagram } of blocks) {
+ try {
+ await mermaid.parse(diagram, { suppressErrors: false });
+ console.log(`${file}:${line}: OK`);
+ } catch (error) {
+ failures++;
+ const message = String(error.message ?? error).split('\n').slice(0, 2).join(' | ');
+ console.error(`${file}:${line}: INVALID - ${message}`);
+ }
+ }
+ console.log(`${blocks.length - failures}/${blocks.length} mermaid block(s) valid.`);
+ if (failures > 0) process.exit(1);
+}
+
+await main();