From 1c87bc833367658dca127ebe50c7ad6cdfdc874a Mon Sep 17 00:00:00 2001 From: Jakob Hahn Date: Mon, 22 Jun 2026 13:45:30 +0000 Subject: [PATCH 1/3] fix(deployment): guard applyProbeOverrides against nil probe Co-Authored-By: Claude --- internal/builders/controlplane/deployment.go | 2 +- internal/builders/controlplane/deployment_test.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/builders/controlplane/deployment.go b/internal/builders/controlplane/deployment.go index 458b5712..85233687 100644 --- a/internal/builders/controlplane/deployment.go +++ b/internal/builders/controlplane/deployment.go @@ -52,7 +52,7 @@ const ( ) func applyProbeOverrides(probe *corev1.Probe, spec *kamajiv1alpha1.ProbeSpec) { - if spec == nil { + if probe == nil || spec == nil { return } diff --git a/internal/builders/controlplane/deployment_test.go b/internal/builders/controlplane/deployment_test.go index 43c6cc5a..52154245 100644 --- a/internal/builders/controlplane/deployment_test.go +++ b/internal/builders/controlplane/deployment_test.go @@ -121,6 +121,11 @@ var _ = Describe("Controlplane Deployment", func() { Expect(probe.InitialDelaySeconds).To(Equal(int32(0))) Expect(probe.SuccessThreshold).To(Equal(int32(1))) }) + + It("should not panic when probe is nil", func() { + spec := &kamajiv1alpha1.ProbeSpec{PeriodSeconds: pointer.To(int32(20))} + Expect(func() { applyProbeOverrides(nil, spec) }).ToNot(Panic()) + }) }) Describe("mergeAPIServerArgs", func() { From 01abe7ecd36480bde35003a353175f4c1f01c0de Mon Sep 17 00:00:00 2001 From: Jakob Hahn Date: Mon, 22 Jun 2026 14:12:55 +0000 Subject: [PATCH 2/3] fix(deployment): render readiness probe for scheduler and controller-manager (#1192) Co-Authored-By: Claude --- internal/builders/controlplane/deployment.go | 171 +++++------------- .../builders/controlplane/deployment_test.go | 79 ++++++++ 2 files changed, 129 insertions(+), 121 deletions(-) diff --git a/internal/builders/controlplane/deployment.go b/internal/builders/controlplane/deployment.go index 85233687..5efd8d98 100644 --- a/internal/builders/controlplane/deployment.go +++ b/internal/builders/controlplane/deployment.go @@ -63,6 +63,44 @@ func applyProbeOverrides(probe *corev1.Probe, spec *kamajiv1alpha1.ProbeSpec) { probe.FailureThreshold = pointer.Deref(spec.FailureThreshold, probe.FailureThreshold) } +// defaultProbe builds the standard HTTPS probe shared by all control plane +// components; only the path and port differ between components and probe types. +func defaultProbe(path string, port int32) *corev1.Probe { + return &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: path, + Port: intstr.FromInt32(port), + Scheme: corev1.URISchemeHTTPS, + }, + }, + InitialDelaySeconds: 0, + TimeoutSeconds: 1, + PeriodSeconds: 10, + SuccessThreshold: 1, + FailureThreshold: 3, + } +} + +// applyProbeSetOverrides applies the global probe overrides first, then the +// per-component overrides, to every probe of the container. Probe pointers that +// are nil are skipped (applyProbeOverrides is nil-safe). +func applyProbeSetOverrides(c *corev1.Container, probes *kamajiv1alpha1.ControlPlaneProbes, set *kamajiv1alpha1.ProbeSet) { + if probes == nil { + return + } + + applyProbeOverrides(c.LivenessProbe, probes.Liveness) + applyProbeOverrides(c.ReadinessProbe, probes.Readiness) + applyProbeOverrides(c.StartupProbe, probes.Startup) + + if set != nil { + applyProbeOverrides(c.LivenessProbe, set.Liveness) + applyProbeOverrides(c.ReadinessProbe, set.Readiness) + applyProbeOverrides(c.StartupProbe, set.Startup) + } +} + type DataStoreOverrides struct { Resource string DataStore kamajiv1alpha1.DataStore @@ -368,43 +406,12 @@ func (d Deployment) buildScheduler(podSpec *corev1.PodSpec, tenantControlPlane k podSpec.Containers[index].Image = tenantControlPlane.Spec.ControlPlane.Deployment.RegistrySettings.KubeSchedulerImage(tenantControlPlane.Spec.Kubernetes.Version) podSpec.Containers[index].Command = []string{"kube-scheduler"} podSpec.Containers[index].Args = utilities.ArgsFromMapToSlice(args) - podSpec.Containers[index].LivenessProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/healthz", - Port: intstr.FromInt32(10259), - Scheme: corev1.URISchemeHTTPS, - }, - }, - InitialDelaySeconds: 0, - TimeoutSeconds: 1, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } - podSpec.Containers[index].StartupProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/healthz", - Port: intstr.FromInt32(10259), - Scheme: corev1.URISchemeHTTPS, - }, - }, - InitialDelaySeconds: 0, - TimeoutSeconds: 1, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } + podSpec.Containers[index].LivenessProbe = defaultProbe("/healthz", 10259) + podSpec.Containers[index].ReadinessProbe = defaultProbe("/healthz", 10259) + podSpec.Containers[index].StartupProbe = defaultProbe("/healthz", 10259) if probes := tenantControlPlane.Spec.ControlPlane.Deployment.Probes; probes != nil { - applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Liveness) - applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Startup) - - if probes.Scheduler != nil { - applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Scheduler.Liveness) - applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Scheduler.Startup) - } + applyProbeSetOverrides(&podSpec.Containers[index], probes, probes.Scheduler) } if containerSecurityContexts := tenantControlPlane.Spec.ControlPlane.Deployment.ContainerSecurityContexts; containerSecurityContexts != nil { @@ -480,43 +487,12 @@ func (d Deployment) buildControllerManager(podSpec *corev1.PodSpec, tenantContro podSpec.Containers[index].Image = tenantControlPlane.Spec.ControlPlane.Deployment.RegistrySettings.KubeControllerManagerImage(tenantControlPlane.Spec.Kubernetes.Version) podSpec.Containers[index].Command = []string{"kube-controller-manager"} podSpec.Containers[index].Args = utilities.ArgsFromMapToSlice(args) - podSpec.Containers[index].LivenessProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/healthz", - Port: intstr.FromInt32(10257), - Scheme: corev1.URISchemeHTTPS, - }, - }, - InitialDelaySeconds: 0, - TimeoutSeconds: 1, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } - podSpec.Containers[index].StartupProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/healthz", - Port: intstr.FromInt32(10257), - Scheme: corev1.URISchemeHTTPS, - }, - }, - InitialDelaySeconds: 0, - TimeoutSeconds: 1, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } + podSpec.Containers[index].LivenessProbe = defaultProbe("/healthz", 10257) + podSpec.Containers[index].ReadinessProbe = defaultProbe("/healthz", 10257) + podSpec.Containers[index].StartupProbe = defaultProbe("/healthz", 10257) if probes := tenantControlPlane.Spec.ControlPlane.Deployment.Probes; probes != nil { - applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Liveness) - applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Startup) - - if probes.ControllerManager != nil { - applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.ControllerManager.Liveness) - applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.ControllerManager.Startup) - } + applyProbeSetOverrides(&podSpec.Containers[index], probes, probes.ControllerManager) } if containerSecurityContexts := tenantControlPlane.Spec.ControlPlane.Deployment.ContainerSecurityContexts; containerSecurityContexts != nil { @@ -614,59 +590,12 @@ func (d Deployment) buildKubeAPIServer(podSpec *corev1.PodSpec, tenantControlPla podSpec.Containers[index].Args = args podSpec.Containers[index].Image = tenantControlPlane.Spec.ControlPlane.Deployment.RegistrySettings.KubeAPIServerImage(tenantControlPlane.Spec.Kubernetes.Version) podSpec.Containers[index].Command = []string{"kube-apiserver"} - podSpec.Containers[index].LivenessProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/livez", - Port: intstr.FromInt32(tenantControlPlane.Spec.NetworkProfile.Port), - Scheme: corev1.URISchemeHTTPS, - }, - }, - InitialDelaySeconds: 0, - TimeoutSeconds: 1, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } - podSpec.Containers[index].ReadinessProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/readyz", - Port: intstr.FromInt32(tenantControlPlane.Spec.NetworkProfile.Port), - Scheme: corev1.URISchemeHTTPS, - }, - }, - InitialDelaySeconds: 0, - TimeoutSeconds: 1, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } - podSpec.Containers[index].StartupProbe = &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/livez", - Port: intstr.FromInt32(tenantControlPlane.Spec.NetworkProfile.Port), - Scheme: corev1.URISchemeHTTPS, - }, - }, - InitialDelaySeconds: 0, - TimeoutSeconds: 1, - PeriodSeconds: 10, - SuccessThreshold: 1, - FailureThreshold: 3, - } + podSpec.Containers[index].LivenessProbe = defaultProbe("/livez", tenantControlPlane.Spec.NetworkProfile.Port) + podSpec.Containers[index].ReadinessProbe = defaultProbe("/readyz", tenantControlPlane.Spec.NetworkProfile.Port) + podSpec.Containers[index].StartupProbe = defaultProbe("/livez", tenantControlPlane.Spec.NetworkProfile.Port) if probes := tenantControlPlane.Spec.ControlPlane.Deployment.Probes; probes != nil { - applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.Liveness) - applyProbeOverrides(podSpec.Containers[index].ReadinessProbe, probes.Readiness) - applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.Startup) - - if probes.APIServer != nil { - applyProbeOverrides(podSpec.Containers[index].LivenessProbe, probes.APIServer.Liveness) - applyProbeOverrides(podSpec.Containers[index].ReadinessProbe, probes.APIServer.Readiness) - applyProbeOverrides(podSpec.Containers[index].StartupProbe, probes.APIServer.Startup) - } + applyProbeSetOverrides(&podSpec.Containers[index], probes, probes.APIServer) } if containerSecurityContexts := tenantControlPlane.Spec.ControlPlane.Deployment.ContainerSecurityContexts; containerSecurityContexts != nil { diff --git a/internal/builders/controlplane/deployment_test.go b/internal/builders/controlplane/deployment_test.go index 52154245..2c19e683 100644 --- a/internal/builders/controlplane/deployment_test.go +++ b/internal/builders/controlplane/deployment_test.go @@ -205,4 +205,83 @@ var _ = Describe("Controlplane Deployment", func() { })) }) }) + + Describe("control plane probes", func() { + // helper: find a container by name in a built PodSpec + containerByName := func(spec *corev1.PodSpec, name string) corev1.Container { + for _, c := range spec.Containers { + if c.Name == name { + return c + } + } + Fail("container not found: " + name) + + return corev1.Container{} + } + + It("renders a readiness probe for kube-scheduler on /healthz:10259", func() { + podSpec := &corev1.PodSpec{} + d.buildScheduler(podSpec, kamajiv1alpha1.TenantControlPlane{}) + + c := containerByName(podSpec, "kube-scheduler") + Expect(c.ReadinessProbe).ToNot(BeNil()) + Expect(c.ReadinessProbe.HTTPGet.Path).To(Equal("/healthz")) + Expect(c.ReadinessProbe.HTTPGet.Port.IntValue()).To(Equal(10259)) + Expect(c.ReadinessProbe.HTTPGet.Scheme).To(Equal(corev1.URISchemeHTTPS)) + Expect(c.ReadinessProbe.PeriodSeconds).To(Equal(int32(10))) + }) + + It("renders a readiness probe for kube-controller-manager on /healthz:10257", func() { + podSpec := &corev1.PodSpec{} + d.buildControllerManager(podSpec, kamajiv1alpha1.TenantControlPlane{}) + + c := containerByName(podSpec, "kube-controller-manager") + Expect(c.ReadinessProbe).ToNot(BeNil()) + Expect(c.ReadinessProbe.HTTPGet.Path).To(Equal("/healthz")) + Expect(c.ReadinessProbe.HTTPGet.Port.IntValue()).To(Equal(10257)) + Expect(c.ReadinessProbe.HTTPGet.Scheme).To(Equal(corev1.URISchemeHTTPS)) + }) + + It("cascades global then component readiness overrides onto the scheduler", func() { + tcp := kamajiv1alpha1.TenantControlPlane{} + tcp.Spec.ControlPlane.Deployment.Probes = &kamajiv1alpha1.ControlPlaneProbes{ + Readiness: &kamajiv1alpha1.ProbeSpec{PeriodSeconds: pointer.To(int32(20))}, + Scheduler: &kamajiv1alpha1.ProbeSet{ + Readiness: &kamajiv1alpha1.ProbeSpec{PeriodSeconds: pointer.To(int32(30))}, + }, + } + + podSpec := &corev1.PodSpec{} + d.buildScheduler(podSpec, tcp) + + c := containerByName(podSpec, "kube-scheduler") + Expect(c.ReadinessProbe.PeriodSeconds).To(Equal(int32(30))) // component wins over global + }) + + It("applies a global-only readiness override to the scheduler", func() { + tcp := kamajiv1alpha1.TenantControlPlane{} + tcp.Spec.ControlPlane.Deployment.Probes = &kamajiv1alpha1.ControlPlaneProbes{ + Readiness: &kamajiv1alpha1.ProbeSpec{PeriodSeconds: pointer.To(int32(20))}, + } + + podSpec := &corev1.PodSpec{} + d.buildScheduler(podSpec, tcp) + + c := containerByName(podSpec, "kube-scheduler") + Expect(c.ReadinessProbe.PeriodSeconds).To(Equal(int32(20))) + }) + + It("leaves the kube-apiserver probes unchanged (regression guard)", func() { + podSpec := &corev1.PodSpec{} + tcp := kamajiv1alpha1.TenantControlPlane{} + tcp.Spec.NetworkProfile.Port = 6443 + d.buildKubeAPIServer(podSpec, tcp, "") + + c := containerByName(podSpec, "kube-apiserver") + Expect(c.LivenessProbe.HTTPGet.Path).To(Equal("/livez")) + Expect(c.ReadinessProbe.HTTPGet.Path).To(Equal("/readyz")) + Expect(c.StartupProbe.HTTPGet.Path).To(Equal("/livez")) + Expect(c.ReadinessProbe.HTTPGet.Port.IntValue()).To(Equal(6443)) + }) + }) }) From 4e8f5d37e50d0d66bf44097a9999d9beadc758fd Mon Sep 17 00:00:00 2001 From: Jakob Hahn Date: Mon, 22 Jun 2026 14:34:51 +0000 Subject: [PATCH 3/3] docs(api): readiness probe defaults apply to all control plane components The global probes.readiness now cascades to scheduler and controller-manager as well as kube-apiserver, matching liveness/startup. Update the godoc and regenerate the CRD descriptions accordingly. Co-Authored-By: Claude --- api/v1alpha1/tenantcontrolplane_types.go | 2 +- .../hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml | 2 +- charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml | 2 +- docs/content/reference/api.md | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/tenantcontrolplane_types.go b/api/v1alpha1/tenantcontrolplane_types.go index 7891a4be..96b8c152 100644 --- a/api/v1alpha1/tenantcontrolplane_types.go +++ b/api/v1alpha1/tenantcontrolplane_types.go @@ -240,7 +240,7 @@ type ProbeSet struct { type ControlPlaneProbes struct { // Liveness defines default parameters for liveness probes of all Control Plane components. Liveness *ProbeSpec `json:"liveness,omitempty"` - // Readiness defines default parameters for the readiness probe of kube-apiserver. + // Readiness defines default parameters for readiness probes of all Control Plane components. Readiness *ProbeSpec `json:"readiness,omitempty"` // Startup defines default parameters for startup probes of all Control Plane components. Startup *ProbeSpec `json:"startup,omitempty"` diff --git a/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml b/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml index f72bbef4..f4936478 100644 --- a/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml +++ b/charts/kamaji-crds/hack/kamaji.clastix.io_tenantcontrolplanes_spec.yaml @@ -7361,7 +7361,7 @@ versions: type: integer type: object readiness: - description: Readiness defines default parameters for the readiness probe of kube-apiserver. + description: Readiness defines default parameters for readiness probes of all Control Plane components. properties: failureThreshold: description: FailureThreshold is the consecutive failure count required to consider the probe failed. diff --git a/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml b/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml index f9c260d2..b74dbb82 100644 --- a/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml +++ b/charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml @@ -7369,7 +7369,7 @@ spec: type: integer type: object readiness: - description: Readiness defines default parameters for the readiness probe of kube-apiserver. + description: Readiness defines default parameters for readiness probes of all Control Plane components. properties: failureThreshold: description: FailureThreshold is the consecutive failure count required to consider the probe failed. diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index 47d3bc3f..b0a1be06 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -44720,7 +44720,7 @@ Override TimeoutSeconds, PeriodSeconds, and FailureThreshold for resource-constr readiness object - Readiness defines default parameters for the readiness probe of kube-apiserver.
+ Readiness defines default parameters for readiness probes of all Control Plane components.
false @@ -45305,7 +45305,7 @@ Must be 1 for liveness and startup probes.
`TenantControlPlane.spec.controlPlane.deployment.probes.readiness` -Readiness defines default parameters for the readiness probe of kube-apiserver. +Readiness defines default parameters for readiness probes of all Control Plane components.