support setting periodSeconds and failureThreashold in the Keyclock CR (#40117)

* add probe spec

Signed-off-by: AvivGuiser <avivguiser@gmail.com>

* make default for probes if not configured, add skeleton test files

Signed-off-by: AvivGuiser <avivguiser@gmail.com>

* fix tests

Signed-off-by: AvivGuiser <avivguiser@gmail.com>

* fix tests

Signed-off-by: AvivGuiser <avivguiser@gmail.com>

* add docs

Signed-off-by: AvivGuiser <avivguiser@gmail.com>

* move test to unittest and apiserver test

Signed-off-by: AvivGuiser <avivguiser@gmail.com>

* adding asserts to check new fields

Signed-off-by: AvivGuiser <avivguiser@gmail.com>

* fix test

Signed-off-by: AvivGuiser <aviv.guiser@placer.ai>

* update docs

Signed-off-by: AvivGuiser <aviv.guiser@placer.ai>

---------

Signed-off-by: AvivGuiser <avivguiser@gmail.com>
Signed-off-by: AvivGuiser <aviv.guiser@placer.ai>
This commit is contained in:
AvivGuiser 2025-06-13 20:32:20 +03:00 committed by GitHub
parent 76bc9fadcb
commit 7736ca20e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 125 additions and 28 deletions

View file

@ -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.

View file

@ -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)

View file

@ -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;
}
}

View file

@ -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;}
}

View file

@ -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;

View file

@ -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

View file

@ -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<KeycloakSpecBuilder> newSpec, Consumer<KeycloakSpecBuilder> oldSpec, Consumer<StatefulSetBuilder> existingModifier) {
// create an existing from the old spec and modifier
StatefulSetBuilder existingBuilder = getDeployment(null, null, oldSpec).toBuilder();

View file

@ -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: