Supported option to specify resource management for pods in Keycloak CR (#26661)

Closes #26456

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Martin Bartoš
2024-02-15 13:38:41 +01:00
committed by GitHub
parent 91f02f1c00
commit 59007844d9
23 changed files with 503 additions and 23 deletions
@@ -17,6 +17,7 @@
package org.keycloak.operator;
import io.fabric8.kubernetes.api.model.Quantity;
import io.smallrye.config.ConfigMapping;
import java.util.Map;
@@ -34,6 +35,16 @@ public interface Config {
boolean startOptimized();
int pollIntervalSeconds();
ResourceRequirements resources();
Map<String, String> podLabels();
}
interface ResourceRequirements {
Resources requests();
Resources limits();
interface Resources {
Quantity memory();
}
}
}
@@ -17,12 +17,14 @@
package org.keycloak.operator;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
import io.quarkus.logging.Log;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import java.nio.charset.StandardCharsets;
@@ -30,6 +32,7 @@ import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
@@ -77,4 +80,35 @@ public final class Utils {
return ies.get(new ResourceID(nameFunction.apply(primary), primary.getMetadata().getNamespace()));
}
/**
* Set resources requests/limits for Keycloak container
* </p>
* If not specified in the Keycloak CR, set default values from operator config
*/
public static void addResources(ResourceRequirements resource, Config config, Container kcContainer) {
final ResourceRequirements resourcesSpec = Optional.ofNullable(resource).orElseGet(ResourceRequirements::new);
// sets the min boundary when the spec is not present
final var requests = Optional.ofNullable(resourcesSpec.getRequests()).orElseGet(HashMap::new);
final var requestsMemory = requests.get("memory");
final var defaultRequestsMemory = config.keycloak().resources().requests().memory();
// Validate 'requests' memory
if (requestsMemory != null) {
var specifiedMemoryIsLessThanDefault = requestsMemory.getNumericalAmount().intValue() < defaultRequestsMemory.getNumericalAmount().intValue();
if (specifiedMemoryIsLessThanDefault) {
Log.debugf("Provided 'requests' memory ('%s') is less than used default value ('%s'). Use it in your risk, as Keycloak performance might be degraded.", requestsMemory, defaultRequestsMemory);
}
} else {
requests.put("memory", defaultRequestsMemory);
}
// sets the max boundary when the spec is not present
final var limits = Optional.ofNullable(resourcesSpec.getLimits()).orElseGet(HashMap::new);
limits.putIfAbsent("memory", config.keycloak().resources().limits().memory());
kcContainer.setResources(resourcesSpec);
}
}
@@ -234,6 +234,9 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
status.addWarningMessage(
"The image of the keycloak container cannot be modified using podTemplate");
}
if (container.getResources() != null) {
status.addWarningMessage("Resources requirements of the Keycloak container cannot be modified using podTemplate");
}
});
if (overlayTemplate.getSpec() != null &&
@@ -22,6 +22,7 @@ import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
import io.fabric8.kubernetes.api.model.EnvVarSource;
import io.fabric8.kubernetes.api.model.EnvVarSourceBuilder;
import io.fabric8.kubernetes.api.model.PodResourceClaim;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodTemplateSpec;
import io.fabric8.kubernetes.api.model.Secret;
@@ -66,6 +67,7 @@ import java.util.stream.Stream;
import jakarta.inject.Inject;
import static org.keycloak.operator.Utils.addResources;
import static org.keycloak.operator.crds.v2alpha1.CRDUtils.isTlsConfigured;
@KubernetesDependent(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
@@ -112,6 +114,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
Container kcContainer = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
addTruststores(primary, baseDeployment, kcContainer, allSecrets);
addEnvVars(baseDeployment, primary, allSecrets);
addResources(primary.getSpec().getResourceRequirements(), operatorConfig, kcContainer);
Optional.ofNullable(primary.getSpec().getCacheSpec())
.ifPresent(c -> configureCache(primary, baseDeployment, kcContainer, c, context.getClient()));
@@ -32,6 +32,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.quarkus.logging.Log;
import org.keycloak.operator.Config;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatus;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImportStatusBuilder;
@@ -49,6 +50,9 @@ dependents = {
})
public class KeycloakRealmImportController implements Reconciler<KeycloakRealmImport>, ErrorStatusHandler<KeycloakRealmImport>, EventSourceInitializer<KeycloakRealmImport> {
@Inject
Config config;
@Inject
KubernetesClient client;
@@ -56,7 +60,7 @@ public class KeycloakRealmImportController implements Reconciler<KeycloakRealmIm
@Override
public Map<String, EventSource> prepareEventSources(EventSourceContext<KeycloakRealmImport> context) {
this.jobDependentResource = new KeycloakRealmImportJobDependentResource();
this.jobDependentResource = new KeycloakRealmImportJobDependentResource(config);
return EventSourceInitializer.nameEventSourcesFromDependentResource(context, jobDependentResource);
}
@@ -31,8 +31,10 @@ import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
import jakarta.inject.Inject;
import org.keycloak.operator.Config;
import org.keycloak.operator.Constants;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
@@ -40,14 +42,19 @@ import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
import java.util.List;
import java.util.Set;
import static org.keycloak.operator.Utils.addResources;
import static org.keycloak.operator.controllers.KeycloakDistConfigurator.getKeycloakOptionEnvVarName;
public class KeycloakRealmImportJobDependentResource extends KubernetesDependentResource<Job, KeycloakRealmImport> implements Creator<Job, KeycloakRealmImport>, GarbageCollected<KeycloakRealmImport> {
KeycloakRealmImportJobDependentResource() {
private final Config config;
KeycloakRealmImportJobDependentResource(Config config) {
super(Job.class);
this.configureWith(new KubernetesDependentResourceConfig<Job>()
.setLabelSelector(Constants.DEFAULT_LABELS_AS_STRING));
this.config = config;
this.configureWith(new KubernetesDependentResourceConfigBuilder<Job>()
.withLabelSelector(Constants.DEFAULT_LABELS_AS_STRING)
.build());
}
@Override
@@ -61,7 +68,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
String secretName = KeycloakRealmImportSecretDependentResource.getSecretName(primary);
String volumeName = KubernetesResourceUtil.sanitizeName(secretName + "-volume");
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0), volumeName, primary.getRealmName());
buildKeycloakJobContainer(keycloakPodTemplate.getSpec().getContainers().get(0), primary, volumeName);
keycloakPodTemplate.getSpec().getVolumes().add(buildSecretVolume(volumeName, secretName));
var labels = keycloakPodTemplate.getMetadata().getLabels();
@@ -114,7 +121,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
.build();
}
private void buildKeycloakJobContainer(Container keycloakContainer, String volumeName, String realmName) {
private void buildKeycloakJobContainer(Container keycloakContainer, KeycloakRealmImport keycloakRealmImport, String volumeName) {
var importMntPath = "/mnt/realm-import/";
var command = List.of("/bin/bash");
@@ -124,7 +131,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
var runBuild = !keycloakContainer.getArgs().contains(KeycloakDeploymentDependentResource.OPTIMIZED_ARG) ? "/opt/keycloak/bin/kc.sh --verbose build && " : "";
var commandArgs = List.of("-c",
runBuild + "/opt/keycloak/bin/kc.sh --verbose import --optimized --file='" + importMntPath + realmName + "-realm.json' " + override);
runBuild + "/opt/keycloak/bin/kc.sh --verbose import --optimized --file='" + importMntPath + keycloakRealmImport.getRealmName() + "-realm.json' " + override);
keycloakContainer.setCommand(command);
keycloakContainer.setArgs(commandArgs);
@@ -139,5 +146,7 @@ public class KeycloakRealmImportJobDependentResource extends KubernetesDependent
// Disable probes since we are not really starting the server
keycloakContainer.setReadinessProbe(null);
keycloakContainer.setLivenessProbe(null);
addResources(keycloakRealmImport.getSpec().getResourceRequirements(), config, keycloakContainer);
}
}
@@ -17,6 +17,7 @@
package org.keycloak.operator.crds.v2alpha1.deployment;
import io.fabric8.kubernetes.api.model.LocalObjectReference;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import io.fabric8.kubernetes.model.annotation.SpecReplicas;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.CacheSpec;
@@ -95,6 +96,10 @@ public class KeycloakSpec {
@JsonPropertyDescription("In this section you can configure Keycloak's cache")
private CacheSpec cacheSpec;
@JsonProperty("resources")
@JsonPropertyDescription("Compute Resources required by Keycloak container")
private ResourceRequirements resourceRequirements;
public HttpSpec getHttpSpec() {
return httpSpec;
}
@@ -213,4 +218,12 @@ public class KeycloakSpec {
this.cacheSpec = cache;
}
public ResourceRequirements getResourceRequirements() {
return resourceRequirements;
}
public void setResourceRequirements(ResourceRequirements resourceRequirements) {
this.resourceRequirements = resourceRequirements;
}
}
@@ -16,8 +16,10 @@
*/
package org.keycloak.operator.crds.v2alpha1.realmimport;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.fabric8.generator.annotation.Required;
import io.fabric8.kubernetes.api.model.ResourceRequirements;
import org.keycloak.representations.idm.RealmRepresentation;
public class KeycloakRealmImportSpec {
@@ -29,6 +31,10 @@ public class KeycloakRealmImportSpec {
@JsonPropertyDescription("The RealmRepresentation to import into Keycloak.")
private RealmRepresentation realm;
@JsonProperty("resources")
@JsonPropertyDescription("Compute Resources required by Keycloak container. If not specified, the value is inherited from the Keycloak CR.")
private ResourceRequirements resourceRequirements;
public String getKeycloakCRName() {
return keycloakCRName;
}
@@ -45,4 +51,11 @@ public class KeycloakRealmImportSpec {
this.realm = realm;
}
public ResourceRequirements getResourceRequirements() {
return resourceRequirements;
}
public void setResourceRequirements(ResourceRequirements resourceRequirements) {
this.resourceRequirements = resourceRequirements;
}
}
@@ -9,6 +9,9 @@ kc.operator.keycloak.image=${RELATED_IMAGE_KEYCLOAK:quay.io/keycloak/keycloak:ni
kc.operator.keycloak.image-pull-policy=Always
kc.operator.keycloak.start-optimized=false
kc.operator.keycloak.poll-interval-seconds=60
# Keycloak container default requests/limits resources
kc.operator.keycloak.resources.requests.memory=768Mi
kc.operator.keycloak.resources.limits.memory=4Gi
# https://quarkus.io/guides/deploying-to-kubernetes#environment-variables-from-keyvalue-pairs
quarkus.kubernetes.env.vars.related-image-keycloak=${kc.operator.keycloak.image}