refactor(lineage-controller-webhook): align with cozystack-api shape
Collapse the chart to a single Deployment shape that mirrors cozystack-api: 2 replicas, soft (preferred) nodeAffinity to node-role.kubernetes.io/control-plane via Exists, permissive tolerations, soft podAntiAffinity on hostname, an unconditional PodDisruptionBudget with maxUnavailable: 1, and a Service with spec.trafficDistribution: PreferClose. The same shape works on Talos / kubeadm / k3s and on managed Kubernetes / Cozy-in-Cozy tenant clusters without per-distro overrides — fixing #2417 by making the soft control-plane affinity gracefully fall back to worker scheduling when no control-plane nodes are visible. Drop the DaemonSet path entirely. The previous PR's deployment.enabled toggle, the workload-kind switch in templates/workload.yaml, the fail-guard for localK8sAPIEndpoint+nodeAffinity=[], the conditional PDB, and the values-shape knobs for nodeAffinity and tolerations all go away as a consequence. Override the standard Deployment fields through the usual component-values mechanism if a non-default topology is ever needed. Mark localK8sAPIEndpoint.enabled deprecated and flip the default to false. The flag injects KUBERNETES_SERVICE_HOST=status.hostIP, which is only valid when the pod is actually scheduled on an apiserver- bearing node. With the new soft control-plane affinity, the pod can land off-control-plane and crash-loop. The latency motivation for the flag is real but pending separate webhook performance work; once addressed, the flag can be removed. Revert all changes to packages/core/platform/images/migrations/migrations/20. The earlier ds/...-or-deploy/... fallback was over-engineered: per run-migrations.sh, migration 20 fires only when CURRENT < 20 (i.e. direct upgrades from pre-0.37 to 1.3+), and that path is unsupported anyway. The original ds/... rollout-status line is dead code on every supported install and upgrade path. Add helm-unittest coverage for: the default Deployment shape (replicas, soft nodeAffinity, soft podAntiAffinity, tolerations); no env vars at the default localK8sAPIEndpoint.enabled=false; PDB rendered unconditionally with maxUnavailable: 1, including at replicas=1 (the no-op case); replicas value drives spec.replicas; and that localK8sAPIEndpoint.enabled=true does inject the env vars. The Service test continues to assert trafficDistribution: PreferClose with no internalTrafficPolicy. Add a slim README documenting the topology, parameters, and the deprecation note for localK8sAPIEndpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
This commit is contained in:
parent
3b35404489
commit
1dc02d44f4
10 changed files with 230 additions and 74 deletions
|
|
@ -105,9 +105,6 @@
|
|||
{{- /* cozystack-api DaemonSet */ -}}
|
||||
{{- $apiValues := dict "cozystackAPI" (dict "nodeSelector" $genericNodeSelector) -}}
|
||||
{{- $_ := set $cozystackEngineComponents "cozystack-api" (dict "values" $apiValues) -}}
|
||||
{{- /* lineage-controller-webhook DaemonSet */ -}}
|
||||
{{- $lineageValues := dict "lineageControllerWebhook" (dict "nodeSelector" $genericNodeSelector) -}}
|
||||
{{- $_ := set $cozystackEngineComponents "lineage-controller-webhook" (dict "values" $lineageValues) -}}
|
||||
{{- end -}}
|
||||
{{- if .Values.authentication.oidc.enabled }}
|
||||
{{include "cozystack.platform.package" (list "cozystack.cozystack-engine" "oidc" $ $cozystackEngineComponents) }}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,13 @@ NAMESPACE=cozy-system
|
|||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
.PHONY: test
|
||||
|
||||
image: image-lineage-controller-webhook
|
||||
|
||||
test:
|
||||
helm unittest .
|
||||
|
||||
image-lineage-controller-webhook:
|
||||
docker buildx build -f images/lineage-controller-webhook/Dockerfile ../../.. \
|
||||
--tag $(REGISTRY)/lineage-controller-webhook:$(call settag,$(TAG)) \
|
||||
|
|
|
|||
67
packages/system/lineage-controller-webhook/README.md
Normal file
67
packages/system/lineage-controller-webhook/README.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# lineage-controller-webhook
|
||||
|
||||
Cozystack system package for the **lineage controller webhook** — a mutating
|
||||
admission webhook that stamps "lineage" labels onto tenant workloads, linking
|
||||
each resource back to the Cozystack `Application` that ultimately owns it.
|
||||
|
||||
The webhook intercepts CREATE and UPDATE on `pods`, `secrets`, `services`,
|
||||
`persistentvolumeclaims`, `ingresses.networking.k8s.io`, and
|
||||
`workloadmonitors.cozystack.io` outside system namespaces. For each request it
|
||||
walks the ownership graph upward (Kubernetes `ownerReferences`, then the
|
||||
`HelmRelease` Flux installed the resource with, then the Cozystack
|
||||
`Application`-derived CRD that produced the HelmRelease) and writes the
|
||||
discovered application's group, kind and name as labels
|
||||
(`apps.cozystack.io/application.{group,kind,name}`) on the incoming object.
|
||||
Those labels let the aggregated API server, the Cozystack dashboard, and other
|
||||
lineage-aware consumers reason about which application a resource belongs to.
|
||||
|
||||
The webhook serves TLS on port 9443 with a cert-manager issued certificate, and
|
||||
runs with `failurePolicy: Fail` — every CREATE/UPDATE on the resources above
|
||||
is gated on the webhook being reachable.
|
||||
|
||||
## Topology
|
||||
|
||||
The chart ships a single shape, modelled on `cozystack-api`:
|
||||
|
||||
- **Deployment** with two replicas (override via `replicas`).
|
||||
- **Soft `nodeAffinity`** preferring `node-role.kubernetes.io/control-plane`
|
||||
(`Exists`, so it matches both Talos's empty value and k3s/kubeadm's `"true"`).
|
||||
Soft means: the pod lands on a control-plane node when one is reachable, and
|
||||
on any worker otherwise — no override needed for managed Kubernetes,
|
||||
Cozy-in-Cozy tenants, or any other cluster where control-plane nodes aren't
|
||||
visible.
|
||||
- **Permissive `tolerations`** (`[{operator: Exists}]`) so the pod can land on
|
||||
tainted control-plane nodes when the soft affinity is satisfiable.
|
||||
- **Soft `podAntiAffinity`** on `kubernetes.io/hostname` so replicas spread
|
||||
across nodes when possible (best-effort).
|
||||
- **`PodDisruptionBudget`** with `maxUnavailable: 1`, unconditional. At
|
||||
`replicas: 1` it's a useful no-op (allows full drain); at `replicas >= 2`
|
||||
it caps disruption to one pod.
|
||||
- **`Service` with `spec.trafficDistribution: PreferClose`** so the apiserver
|
||||
prefers a webhook endpoint on its own node when one exists, and transparently
|
||||
falls over to a remote endpoint otherwise. Requires Kubernetes ≥ 1.31; on
|
||||
older clusters the field is silently ignored and traffic uses default
|
||||
cluster-wide distribution.
|
||||
|
||||
## Parameters
|
||||
|
||||
All keys live under the top-level `lineageControllerWebhook:` map.
|
||||
|
||||
| Name | Description | Value |
|
||||
| ----------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `image` | Container image (digest-pinned) | `ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.3.0@sha256:e898…6fb0` |
|
||||
| `debug` | Enable `--zap-log-level=debug` instead of `info` | `false` |
|
||||
| `replicas` | Deployment replica count | `2` |
|
||||
| `localK8sAPIEndpoint.enabled` | **Deprecated.** See note below. | `false` |
|
||||
|
||||
### `localK8sAPIEndpoint.enabled` (deprecated)
|
||||
|
||||
When enabled, this injects `KUBERNETES_SERVICE_HOST=status.hostIP` and
|
||||
`KUBERNETES_SERVICE_PORT=6443` so the webhook talks to the kube-apiserver on
|
||||
its own node. It was originally added to avoid latency on the
|
||||
webhook-to-apiserver path, but it is only valid when the pod is actually
|
||||
scheduled on an apiserver-bearing node — which the chart's soft control-plane
|
||||
affinity no longer guarantees. With this flag enabled and the pod scheduled
|
||||
off a control-plane node, the controller will crash-loop dialing a non-
|
||||
apiserver hostIP. Slated for removal once the latency motivation is addressed
|
||||
in the webhook itself; **leave disabled**.
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
{{- if .Values.lineageControllerWebhook.deployment.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: lineage-controller-webhook
|
||||
labels:
|
||||
app: lineage-controller-webhook
|
||||
spec:
|
||||
replicas: {{ .Values.lineageControllerWebhook.deployment.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: lineage-controller-webhook
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: lineage-controller-webhook
|
||||
spec:
|
||||
serviceAccountName: lineage-controller-webhook
|
||||
affinity:
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: lineage-controller-webhook
|
||||
containers:
|
||||
- name: lineage-controller-webhook
|
||||
image: "{{ .Values.lineageControllerWebhook.image }}"
|
||||
args:
|
||||
{{- if .Values.lineageControllerWebhook.debug }}
|
||||
- --zap-log-level=debug
|
||||
{{- else }}
|
||||
- --zap-log-level=info
|
||||
{{- end }}
|
||||
ports:
|
||||
- name: webhook
|
||||
containerPort: 9443
|
||||
volumeMounts:
|
||||
- name: webhook-certs
|
||||
mountPath: /tmp/k8s-webhook-server/serving-certs
|
||||
readOnly: true
|
||||
volumes:
|
||||
- name: webhook-certs
|
||||
secret:
|
||||
secretName: lineage-controller-webhook-cert
|
||||
defaultMode: 0400
|
||||
{{- end }}
|
||||
|
|
@ -1,11 +1,9 @@
|
|||
{{- if .Values.lineageControllerWebhook.deployment.enabled }}
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: lineage-controller-webhook
|
||||
spec:
|
||||
minAvailable: {{ if gt (int .Values.lineageControllerWebhook.deployment.replicas) 1 }}1{{ else }}0{{ end }}
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: lineage-controller-webhook
|
||||
{{- end }}
|
||||
|
|
@ -5,7 +5,7 @@ metadata:
|
|||
labels:
|
||||
app: lineage-controller-webhook
|
||||
spec:
|
||||
internalTrafficPolicy: Local
|
||||
trafficDistribution: PreferClose
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 443
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{{- if not .Values.lineageControllerWebhook.deployment.enabled }}
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: lineage-controller-webhook
|
||||
labels:
|
||||
app: lineage-controller-webhook
|
||||
spec:
|
||||
replicas: {{ .Values.lineageControllerWebhook.replicas }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: lineage-controller-webhook
|
||||
|
|
@ -14,13 +14,25 @@ spec:
|
|||
labels:
|
||||
app: lineage-controller-webhook
|
||||
spec:
|
||||
{{- with .Values.lineageControllerWebhook.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
serviceAccountName: lineage-controller-webhook
|
||||
tolerations:
|
||||
- operator: Exists
|
||||
serviceAccountName: lineage-controller-webhook
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
topologyKey: kubernetes.io/hostname
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: lineage-controller-webhook
|
||||
containers:
|
||||
- name: lineage-controller-webhook
|
||||
image: "{{ .Values.lineageControllerWebhook.image }}"
|
||||
|
|
@ -52,4 +64,3 @@ spec:
|
|||
secret:
|
||||
secretName: lineage-controller-webhook-cert
|
||||
defaultMode: 0400
|
||||
{{- end }}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
suite: lineage-controller-webhook service
|
||||
templates:
|
||||
- templates/service.yaml
|
||||
|
||||
release:
|
||||
name: lineage-controller-webhook
|
||||
namespace: cozy-system
|
||||
|
||||
tests:
|
||||
- it: renders a ClusterIP service on 443 -> 9443 with PreferClose traffic distribution
|
||||
asserts:
|
||||
- isKind:
|
||||
of: Service
|
||||
- equal:
|
||||
path: spec.type
|
||||
value: ClusterIP
|
||||
- equal:
|
||||
path: spec.trafficDistribution
|
||||
value: PreferClose
|
||||
- notExists:
|
||||
path: spec.internalTrafficPolicy
|
||||
- equal:
|
||||
path: spec.ports[0].port
|
||||
value: 443
|
||||
- equal:
|
||||
path: spec.ports[0].targetPort
|
||||
value: 9443
|
||||
- equal:
|
||||
path: spec.selector.app
|
||||
value: lineage-controller-webhook
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
suite: lineage-controller-webhook workload + PDB
|
||||
templates:
|
||||
- templates/workload.yaml
|
||||
- templates/pdb.yaml
|
||||
|
||||
release:
|
||||
name: lineage-controller-webhook
|
||||
namespace: cozy-system
|
||||
|
||||
tests:
|
||||
# ---- Default Deployment shape ----------------------------------------------
|
||||
|
||||
- it: defaults render a 2-replica Deployment with soft control-plane nodeAffinity, soft podAntiAffinity, and Exists tolerations
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- isKind:
|
||||
of: Deployment
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 2
|
||||
- equal:
|
||||
path: spec.template.spec.tolerations
|
||||
value:
|
||||
- operator: Exists
|
||||
- equal:
|
||||
path: spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution
|
||||
value:
|
||||
- weight: 100
|
||||
preference:
|
||||
matchExpressions:
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
- notExists:
|
||||
path: spec.template.spec.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution
|
||||
- equal:
|
||||
path: spec.template.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution[0].podAffinityTerm.topologyKey
|
||||
value: kubernetes.io/hostname
|
||||
|
||||
- it: defaults inject no localK8sAPIEndpoint env vars (knob is opt-in)
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.template.spec.containers[0].env
|
||||
|
||||
# ---- PDB --------------------------------------------------------------------
|
||||
|
||||
- it: PDB renders unconditionally with maxUnavailable=1 at the default replica count
|
||||
template: templates/pdb.yaml
|
||||
asserts:
|
||||
- isKind:
|
||||
of: PodDisruptionBudget
|
||||
- equal:
|
||||
path: spec.maxUnavailable
|
||||
value: 1
|
||||
- notExists:
|
||||
path: spec.minAvailable
|
||||
|
||||
- it: PDB still renders at replicas=1 (no-op, but consistent shape)
|
||||
set:
|
||||
lineageControllerWebhook.replicas: 1
|
||||
template: templates/pdb.yaml
|
||||
asserts:
|
||||
- isKind:
|
||||
of: PodDisruptionBudget
|
||||
- equal:
|
||||
path: spec.maxUnavailable
|
||||
value: 1
|
||||
|
||||
# ---- Replica override -------------------------------------------------------
|
||||
|
||||
- it: replicas value drives spec.replicas
|
||||
set:
|
||||
lineageControllerWebhook.replicas: 5
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.replicas
|
||||
value: 5
|
||||
|
||||
# ---- Deprecated localK8sAPIEndpoint knob -----------------------------------
|
||||
|
||||
- it: localK8sAPIEndpoint.enabled=true injects KUBERNETES_SERVICE_HOST and PORT env vars
|
||||
set:
|
||||
lineageControllerWebhook.localK8sAPIEndpoint.enabled: true
|
||||
template: templates/workload.yaml
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: KUBERNETES_SERVICE_HOST
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: status.hostIP
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: KUBERNETES_SERVICE_PORT
|
||||
value: "6443"
|
||||
|
|
@ -1,15 +1,13 @@
|
|||
lineageControllerWebhook:
|
||||
image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.3.0@sha256:e8984709686a5eaf19b89da378d7b8c688ea5607e0783a88d9c9e4ccfca96fb0
|
||||
debug: false
|
||||
replicas: 2
|
||||
# DEPRECATED. Injects KUBERNETES_SERVICE_HOST=status.hostIP and
|
||||
# KUBERNETES_SERVICE_PORT=6443 so the webhook talks to the apiserver on its
|
||||
# own node — only valid when the pod is actually scheduled on an apiserver-
|
||||
# bearing node. Now that the chart's nodeAffinity to control-plane is soft
|
||||
# (preferred, not required), the pod can land off-control-plane and crash-
|
||||
# loop dialing a non-apiserver hostIP. Slated for removal once webhook
|
||||
# performance work obviates the locality motivation. Leave disabled.
|
||||
localK8sAPIEndpoint:
|
||||
# incompatable with Deployment mode
|
||||
enabled: true
|
||||
# nodeSelector for the DaemonSet
|
||||
# Talos uses empty value: "node-role.kubernetes.io/control-plane": ""
|
||||
# Generic k8s (k3s, kubeadm) uses: "node-role.kubernetes.io/control-plane": "true"
|
||||
nodeSelector:
|
||||
node-role.kubernetes.io/control-plane: ""
|
||||
# if enabled, replace DaemonSet by Deployment
|
||||
deployment:
|
||||
enabled: false
|
||||
replicas: 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue