From 7736ca20e9d55bb8d58d79cd8fc08c5c8bb01899 Mon Sep 17 00:00:00 2001 From: AvivGuiser Date: Fri, 13 Jun 2025 20:32:20 +0300 Subject: [PATCH] support setting periodSeconds and failureThreashold in the Keyclock CR (#40117) * add probe spec Signed-off-by: AvivGuiser * make default for probes if not configured, add skeleton test files Signed-off-by: AvivGuiser * fix tests Signed-off-by: AvivGuiser * fix tests Signed-off-by: AvivGuiser * add docs Signed-off-by: AvivGuiser * move test to unittest and apiserver test Signed-off-by: AvivGuiser * adding asserts to check new fields Signed-off-by: AvivGuiser * fix test Signed-off-by: AvivGuiser * update docs Signed-off-by: AvivGuiser --------- Signed-off-by: AvivGuiser Signed-off-by: AvivGuiser --- .../operator/advanced-configuration.adoc | 33 +++++++------------ .../KeycloakDeploymentDependentResource.java | 16 +++++---- .../v2alpha1/deployment/KeycloakSpec.java | 33 +++++++++++++++++++ .../v2alpha1/deployment/spec/ProbeSpec.java | 22 +++++++++++++ .../integration/KeycloakDeploymentTest.java | 1 + .../testsuite/unit/CRSerializationTest.java | 8 +++++ .../testsuite/unit/PodTemplateTest.java | 31 +++++++++++++++++ .../test-serialization-keycloak-cr.yml | 9 +++++ 8 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/ProbeSpec.java diff --git a/docs/guides/operator/advanced-configuration.adoc b/docs/guides/operator/advanced-configuration.adoc index faef511f428..5e274d01357 100644 --- a/docs/guides/operator/advanced-configuration.adoc +++ b/docs/guides/operator/advanced-configuration.adoc @@ -149,15 +149,9 @@ spec: secretName: keycloak-additional-secret ---- -===== Probe Timeouts +===== Probe Configuration -The unsupported podTemplate may be used to override the default probes. - -In particular the default startup probe timeout of 10 minutes may be too short in scenarios where there is a long-running migration. - -If your instances encounter this startup failure or if you wish to proactively prevent such a startup failure from occurring, then the startup probe timeout should be increased. - -With otherwise default settings, something like the following increases the timeout to 20 minutes: +The Keycloak CR exposes options to set periodSeconds and failureThreshold on each of the three probes (readiness, liveness and startup) [source,yaml] ---- @@ -166,22 +160,17 @@ kind: Keycloak metadata: name: example-kc spec: - ... - unsupported: - podTemplate: - spec: - containers: - startupProbe: - httpGet: - path: "/health/started" - port: 9000 - scheme: "HTTPS" - failureThreshold: 1200 - periodSeconds: 1 + readinessProbe: + periodSeconds: 20 + failureThreshold: 5 + livenessProbe: + periodSeconds: 20 + failureThreshold: 5 + startupProbe: + periodSeconds: 20 + failureThreshold: 5 ---- -Note that the usage of a relative HTTP path, or an alternative management port, requires changes to the probe configuration. - === Disabling required options {project_name} and the {project_name} Operator provide the best production-ready experience with security in mind. diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java index abe39e6603a..17a29b9dd7a 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java @@ -48,6 +48,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProbeSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.SchedulingSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.Truststore; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TruststoreSource; @@ -327,6 +328,9 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent // probes var protocol = isTlsConfigured(keycloakCR) ? "HTTPS" : "HTTP"; var port = HttpManagementSpec.managementPort(keycloakCR); + var readinessOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getReadinessProbeSpec()); + var livenessOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getLivenessProbeSpec()); + var startupOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getStartupProbeSpec()); var relativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_MANAGEMENT_RELATIVE_PATH_KEY, keycloakCR, context) .or(() -> readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY, keycloakCR, context)) .map(path -> !path.endsWith("/") ? path + "/" : path) @@ -334,8 +338,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent if (!containerBuilder.hasReadinessProbe()) { containerBuilder.withNewReadinessProbe() - .withPeriodSeconds(10) - .withFailureThreshold(3) + .withPeriodSeconds(readinessOptionalSpec.map(ProbeSpec::getProbePeriodSeconds).orElse(10)) + .withFailureThreshold(readinessOptionalSpec.map(ProbeSpec::getProbeFailureThreshold).orElse(3)) .withNewHttpGet() .withScheme(protocol) .withNewPort(port) @@ -345,8 +349,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent } if (!containerBuilder.hasLivenessProbe()) { containerBuilder.withNewLivenessProbe() - .withPeriodSeconds(10) - .withFailureThreshold(3) + .withPeriodSeconds(livenessOptionalSpec.map(ProbeSpec::getProbePeriodSeconds).orElse(10)) + .withFailureThreshold(livenessOptionalSpec.map(ProbeSpec::getProbeFailureThreshold).orElse(3)) .withNewHttpGet() .withScheme(protocol) .withNewPort(port) @@ -356,8 +360,8 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent } if (!containerBuilder.hasStartupProbe()) { containerBuilder.withNewStartupProbe() - .withPeriodSeconds(1) - .withFailureThreshold(600) + .withPeriodSeconds(startupOptionalSpec.map(ProbeSpec::getProbePeriodSeconds).orElse(1)) + .withFailureThreshold(startupOptionalSpec.map(ProbeSpec::getProbeFailureThreshold).orElse(600)) .withNewHttpGet() .withScheme(protocol) .withNewPort(port) diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java index 09ededf229c..76a9d0ca59e 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java @@ -29,6 +29,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.IngressSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.NetworkPolicySpec; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProbeSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProxySpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.SchedulingSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.TracingSpec; @@ -135,6 +136,20 @@ public class KeycloakSpec { @JsonPropertyDescription("Configuration related to Keycloak deployment updates.") private UpdateSpec updateSpec; + @JsonProperty("readinessProbe") + @JsonPropertyDescription("Configuration for readiness probe, by default it is 10 for periodSeconds and 3 for failureThreshold") + private ProbeSpec readinessProbeSpec; + + + @JsonProperty("livenessProbe") + @JsonPropertyDescription("Configuration for liveness probe, by default it is 10 for periodSeconds and 3 for failureThreshold") + private ProbeSpec livenessProbeSpec; + + @JsonProperty("startupProbe") + @JsonPropertyDescription("Configuration for startup probe, by default it is 1 for periodSeconds and 600 for failureThreshold") + private ProbeSpec startupProbeSpec; + + public HttpSpec getHttpSpec() { return httpSpec; } @@ -316,4 +331,22 @@ public class KeycloakSpec { public void setUpdateSpec(UpdateSpec updateSpec) { this.updateSpec = updateSpec; } + + public ProbeSpec getLivenessProbeSpec() {return livenessProbeSpec;} + + public void setLivenessProbeSpec(ProbeSpec livenessProbeSpec) { + this.livenessProbeSpec = livenessProbeSpec; + } + + public ProbeSpec getReadinessProbeSpec() {return readinessProbeSpec;} + + public void setReadinessProbeSpec(ProbeSpec readinessProbeSpec) { + this.readinessProbeSpec = readinessProbeSpec; + } + + public ProbeSpec getStartupProbeSpec() {return startupProbeSpec;} + + public void setStartupProbeSpec(ProbeSpec startupProbeSpec) { + this.startupProbeSpec = startupProbeSpec; + } } diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/ProbeSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/ProbeSpec.java new file mode 100644 index 00000000000..21f873442fd --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/ProbeSpec.java @@ -0,0 +1,22 @@ +package org.keycloak.operator.crds.v2alpha1.deployment.spec; + + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.sundr.builder.annotations.Buildable; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder") +public class ProbeSpec { + + @JsonProperty("periodSeconds") + private int probePeriodSeconds; + + @JsonProperty("failureThreshold") + private int probeFailureThreshold; + + public int getProbeFailureThreshold() {return probeFailureThreshold;} + public void setProbeFailureThreshold(int probeFailureThreshold) {this.probeFailureThreshold = probeFailureThreshold;} + public int getProbePeriodSeconds() {return probePeriodSeconds;} + public void setProbePeriodSeconds(int probePeriodSeconds) {this.probePeriodSeconds = probePeriodSeconds;} +} diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java index 37ba2d31148..453d3353e51 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java @@ -47,6 +47,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition; import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder; +import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProbeSpec; import org.keycloak.operator.testsuite.apiserver.DisabledIfApiServerTest; import org.keycloak.operator.testsuite.unit.WatchedResourcesTest; import org.keycloak.operator.testsuite.utils.CRAssert; diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java index 063e12c929c..9ff22668d7b 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java @@ -102,6 +102,14 @@ public class CRSerializationTest { HttpManagementSpec managementSpec = keycloak.getSpec().getHttpManagementSpec(); assertNotNull(managementSpec); assertEquals(9003, managementSpec.getPort()); + + assertEquals(50,keycloak.getSpec().getReadinessProbeSpec().getProbePeriodSeconds()); + assertEquals(3,keycloak.getSpec().getReadinessProbeSpec().getProbeFailureThreshold()); + assertEquals(60,keycloak.getSpec().getLivenessProbeSpec().getProbePeriodSeconds()); + assertEquals(1,keycloak.getSpec().getLivenessProbeSpec().getProbeFailureThreshold()); + assertEquals(40,keycloak.getSpec().getStartupProbeSpec().getProbePeriodSeconds()); + assertEquals(2,keycloak.getSpec().getStartupProbeSpec().getProbeFailureThreshold()); + } @Test diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java index 280352f5929..475254ed972 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/PodTemplateTest.java @@ -676,6 +676,37 @@ public class PodTemplateTest { assertThat(podTemplate.getSpec().getAffinity()).isNotEqualTo(affinity); } + @Test + public void testProbe(){ + PodTemplateSpec additionalPodTemplate = null; + var readinessProbe = new ProbeBuilder().withFailureThreshold(1).withPeriodSeconds(2).build(); + var livenessProbe = new ProbeBuilder().withFailureThreshold(3).withPeriodSeconds(4).build(); + var startupProbe = new ProbeBuilder().withFailureThreshold(5).withPeriodSeconds(6).build(); + var readinessPodTemplate = getDeployment(additionalPodTemplate, null, + s-> s.withNewReadinessProbeSpec() + .withProbeFailureThreshold(1) + .withProbePeriodSeconds(2) + .endReadinessProbeSpec()).getSpec().getTemplate(); + assertThat(readinessPodTemplate.getSpec().getContainers().get(0).getReadinessProbe().getPeriodSeconds()).isEqualTo(readinessProbe.getPeriodSeconds()); + assertThat(readinessPodTemplate.getSpec().getContainers().get(0).getReadinessProbe().getFailureThreshold()).isEqualTo(readinessProbe.getFailureThreshold()); + + var livenessPodTemplate = getDeployment(additionalPodTemplate, null, + s-> s.withNewLivenessProbeSpec() + .withProbeFailureThreshold(3) + .withProbePeriodSeconds(4) + .endLivenessProbeSpec()).getSpec().getTemplate(); + assertThat(livenessPodTemplate.getSpec().getContainers().get(0).getLivenessProbe().getPeriodSeconds()).isEqualTo(livenessProbe.getPeriodSeconds()); + assertThat(livenessPodTemplate.getSpec().getContainers().get(0).getLivenessProbe().getFailureThreshold()).isEqualTo(livenessProbe.getFailureThreshold()); + + var startupPodTemplate = getDeployment(additionalPodTemplate, null, + s-> s.withNewStartupProbeSpec() + .withProbeFailureThreshold(5) + .withProbePeriodSeconds(6) + .endStartupProbeSpec()).getSpec().getTemplate(); + assertThat(startupPodTemplate.getSpec().getContainers().get(0).getStartupProbe().getPeriodSeconds()).isEqualTo(startupProbe.getPeriodSeconds()); + assertThat(startupPodTemplate.getSpec().getContainers().get(0).getStartupProbe().getFailureThreshold()).isEqualTo(startupProbe.getFailureThreshold()); + } + private Job getUpdateJob(Consumer newSpec, Consumer oldSpec, Consumer existingModifier) { // create an existing from the old spec and modifier StatefulSetBuilder existingBuilder = getDeployment(null, null, oldSpec).toBuilder(); diff --git a/operator/src/test/resources/test-serialization-keycloak-cr.yml b/operator/src/test/resources/test-serialization-keycloak-cr.yml index 1571a180d47..ba2e3ca666c 100644 --- a/operator/src/test/resources/test-serialization-keycloak-cr.yml +++ b/operator/src/test/resources/test-serialization-keycloak-cr.yml @@ -32,6 +32,15 @@ spec: annotations: myAnnotation: myValue anotherAnnotation: anotherValue + readinessProbe: + periodSeconds: 50 + failureThreshold: 3 + livenessProbe: + periodSeconds: 60 + failureThreshold: 1 + startupProbe: + periodSeconds: 40 + failureThreshold: 2 networkPolicy: enabled: true http: