fix: adding imagePullSecret to update job

closes: #39015

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
(cherry picked from commit 26de9efff334f70dbfc3bcbe7f0c6417afa3a32e)
This commit is contained in:
Steven Hawkins 2025-04-17 15:18:44 -04:00 committed by GitHub
parent d35792d39e
commit d46b72267a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -17,15 +17,18 @@
package org.keycloak.operator.controllers;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.keycloak.operator.Constants;
import org.keycloak.operator.ContextUtils;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.CRDUtils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import io.fabric8.kubernetes.api.model.ContainerFluent;
import io.fabric8.kubernetes.api.model.ObjectMeta;
@ -33,6 +36,7 @@ import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.fabric8.kubernetes.api.model.PodSpec;
import io.fabric8.kubernetes.api.model.PodSpecBuilder;
import io.fabric8.kubernetes.api.model.Volume;
import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.api.model.batch.v1.Job;
import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder;
import io.fabric8.kubernetes.api.model.batch.v1.JobSpecFluent;
@ -41,18 +45,13 @@ import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfigBuilder;
import jakarta.enterprise.context.ApplicationScoped;
import org.keycloak.operator.Constants;
import org.keycloak.operator.ContextUtils;
import org.keycloak.operator.Utils;
import org.keycloak.operator.crds.v2alpha1.CRDUtils;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
@ApplicationScoped
public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentResource<Job, Keycloak> {
// shared volume configuration
private static final String WORK_DIR_VOLUME_NAME = "workdir";
private static final String WORK_DIR_VOLUME_MOUNT_PATH = "/mnt/workdir";
private static final String WORK_DIR_VOLUME_NAME = "keycloak-update-job-temporary-workdir"; // unlikely to conflict
private static final String WORK_DIR_VOLUME_MOUNT_PATH = "/mnt/" + WORK_DIR_VOLUME_NAME; // unlikely to conflict
private static final String UPDATES_FILE_PATH = WORK_DIR_VOLUME_MOUNT_PATH + "/updates.json";
// Annotations
@ -70,9 +69,6 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR
// Job time to live
private static final int JOB_TIME_TO_LIVE_SECONDS = (int) TimeUnit.MINUTES.toSeconds(30);
// container args to replace
private static final Set<String> START_ARGS = Set.of("start", "start-dev");
public KeycloakUpdateJobDependentResource() {
super(Job.class);
this.configureWith(new KubernetesDependentResourceConfigBuilder<Job>()
@ -127,21 +123,36 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR
}
private PodSpec createPodSpec(Context<Keycloak> context) {
var allVolumes = getAllVolumes(context);
Collection<String> requiredVolumes = new HashSet<>();
var builder = new PodSpecBuilder();
StatefulSet current = ContextUtils.getCurrentStatefulSet(context).orElseThrow();
StatefulSet desired = ContextUtils.getDesiredStatefulSet(context);
// start off with the desired statefulset state
var builder = desired.getSpec().getTemplate().getSpec().edit();
builder.withRestartPolicy("Never");
addInitContainer(builder, context, allVolumes.keySet(), requiredVolumes);
addContainer(builder, context, allVolumes.keySet(), requiredVolumes);
// remove things we don't want - the main keycloak container, and any sidecars added via the unsupported PodTemplate
builder.withContainers();
// We'll leave the scheduling fields alone - it should be fine to run these jobs where ever the keycloak workload is restricted to,
// if there is some corner case we are not considering that would leave the upgrade job unscheduable, we'll address that later
// mix in the existing state
var desiredPullSecrets = builder.buildImagePullSecrets();
current.getSpec().getTemplate().getSpec().getImagePullSecrets().stream().filter(s -> !desiredPullSecrets.contains(s)).forEach(builder::addToImagePullSecrets);
// TODO: if the name is the same, but the volume has changed this merging behavior will be inconsistent / incorrect. For example is someone changes which
// configmap the cache config is using
var desiredVolumes = builder.buildVolumes().stream().map(Volume::getName).collect(Collectors.toSet());
current.getSpec().getTemplate().getSpec().getVolumes().stream().filter(v -> !desiredVolumes.contains(v.getName())).forEach(builder::addToVolumes);
// TODO: what else should get merged - there could be additional stuff from the unsupported PodTemplate
addInitContainer(builder, current);
addContainer(builder, desired);
builder.addNewVolume()
.withName(WORK_DIR_VOLUME_NAME)
.withNewEmptyDir()
.endEmptyDir()
.endVolume();
// add volumes to the pod
requiredVolumes.stream()
.map(allVolumes::get)
.forEach(volume -> builder.addNewVolumeLike(volume).endVolume());
// For test KeycloakDeploymentTest#testDeploymentDurability
// it uses a pause image, which never ends.
// After this seconds, the job is terminated allowing the test to complete.
@ -149,34 +160,28 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR
return builder.build();
}
private static void addInitContainer(PodSpecBuilder builder, Context<Keycloak> context, Collection<String> availableVolumes, Collection<String> requiredVolumes) {
var existing = CRDUtils.firstContainerOf(ContextUtils.getCurrentStatefulSet(context).orElseThrow()).orElseThrow();
private static void addInitContainer(PodSpecBuilder builder, StatefulSet current) {
var existing = CRDUtils.firstContainerOf(current).orElseThrow();
var containerBuilder = builder.addNewInitContainerLike(existing);
configureContainer(containerBuilder, INIT_CONTAINER_NAME, INIT_CONTAINER_ARGS, availableVolumes, requiredVolumes);
configureContainer(containerBuilder, INIT_CONTAINER_NAME, INIT_CONTAINER_ARGS);
containerBuilder.endInitContainer();
}
private static void addContainer(PodSpecBuilder builder, Context<Keycloak> context, Collection<String> availableVolumes, Collection<String> requiredVolumes) {
var existing = CRDUtils.firstContainerOf(ContextUtils.getDesiredStatefulSet(context)).orElseThrow();
private static void addContainer(PodSpecBuilder builder, StatefulSet desired) {
var existing = CRDUtils.firstContainerOf(desired).orElseThrow();
var containerBuilder = builder.addNewContainerLike(existing);
configureContainer(containerBuilder, CONTAINER_NAME, CONTAINER_ARGS, availableVolumes, requiredVolumes);
configureContainer(containerBuilder, CONTAINER_NAME, CONTAINER_ARGS);
containerBuilder.endContainer();
}
private static void configureContainer(ContainerFluent<?> containerBuilder, String name, List<String> args, Collection<String> availableVolumes, Collection<String> requiredVolumes) {
private static void configureContainer(ContainerFluent<?> containerBuilder, String name, List<String> args) {
containerBuilder.withName(name);
containerBuilder.withArgs(replaceStartWithUpdateCommand(containerBuilder.getArgs(), args));
// remove volume devices
containerBuilder.withVolumeDevices();
// add existing volume mounts
var volumeMounts = containerBuilder.buildVolumeMounts();
if (volumeMounts != null) {
var newVolumeMounts = volumeMounts.stream()
.filter(volumeMount -> availableVolumes.contains(volumeMount.getName()))
.filter(volumeMount -> !volumeMount.getName().startsWith("kube-api"))
.peek(volumeMount -> requiredVolumes.add(volumeMount.getName()))
.toList();
containerBuilder.withVolumeMounts(newVolumeMounts);
}
@ -194,24 +199,10 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR
.endVolumeMount();
}
private Map<String, Volume> getAllVolumes(Context<Keycloak> context) {
Map<String, Volume> allVolumes = new HashMap<>();
Consumer<Volume> volumeConsumer = volume -> allVolumes.put(volume.getName(), volume);
CRDUtils.volumesFromStatefulSet(ContextUtils.getCurrentStatefulSet(context).orElseThrow()).forEach(volumeConsumer);
CRDUtils.volumesFromStatefulSet(ContextUtils.getDesiredStatefulSet(context)).forEach(volumeConsumer);
return allVolumes;
}
private static List<String> replaceStartWithUpdateCommand(List<String> currentArgs, List<String> updateArgs) {
return currentArgs.stream().
<String>mapMulti((arg, downstream) -> {
if (START_ARGS.contains(arg)) {
updateArgs.forEach(downstream);
return;
}
downstream.accept(arg);
}).toList();
// note that using start-dev via the unsupported podTemplate will fail - that is fine as rolling updates shouldn't apply
// TODO: reuse ConfigArgConfigSource parsing, so that we don't confuse what the command is
return Stream.concat(updateArgs.stream(), currentArgs.stream().filter(arg -> !arg.equals("start"))).toList();
}
private static String keycloakHash(Keycloak keycloak) {