diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java index a3f883e341f..721832ebd24 100644 --- a/operator/src/main/java/org/keycloak/operator/Constants.java +++ b/operator/src/main/java/org/keycloak/operator/Constants.java @@ -39,6 +39,7 @@ public final class Constants { public static final String KEYCLOAK_RECREATE_UPDATE_ANNOTATION = "operator.keycloak.org/recreate-update"; public static final String KEYCLOAK_UPDATE_REASON_ANNOTATION = "operator.keycloak.org/update-reason"; public static final String KEYCLOAK_UPDATE_REVISION_ANNOTATION = "operator.keycloak.org/update-revision"; + public static final String KEYCLOAK_UPDATE_HASH_ANNOTATION = "operator.keycloak.org/update-hash"; public static final String APP_LABEL = "app"; public static final String DEFAULT_LABELS_AS_STRING = "app=keycloak,app.kubernetes.io/managed-by=keycloak-operator"; diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java index 5236bf98a07..505daf42362 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java @@ -90,6 +90,8 @@ public class KeycloakController implements Reconciler { @Inject KeycloakUpdateJobDependentResource updateJobDependentResource; + KeycloakDeploymentDependentResource keycloakDeploymentDependentResource = new KeycloakDeploymentDependentResource(); + @Override public List> prepareEventSources(EventSourceContext context) { return EventSourceUtils.dependentEventSources(context, updateJobDependentResource); @@ -139,7 +141,7 @@ public class KeycloakController implements Reconciler { ContextUtils.storeWatchedResources(context, watchedResources); ContextUtils.storeDistConfigurator(context, distConfigurator); ContextUtils.storeCurrentStatefulSet(context, existingDeployment); - ContextUtils.storeDesiredStatefulSet(context, new KeycloakDeploymentDependentResource().desired(kc, context)); + ContextUtils.storeDesiredStatefulSet(context, keycloakDeploymentDependentResource.initialDesired(kc, context)); var updateLogic = updateLogicFactory.create(kc, context); var updateLogicControl = updateLogic.decideUpdate(); 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 ec9d102a49f..a16822c4983 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java @@ -127,8 +127,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent this.useServiceCaCrt = useServiceCaCrt; } - @Override - public StatefulSet desired(Keycloak primary, Context context) { + public StatefulSet initialDesired(Keycloak primary, Context context) { Config operatorConfig = ContextUtils.getOperatorConfig(context); WatchedResources watchedResources = ContextUtils.getWatchedResources(context); @@ -150,6 +149,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent // default to the new revision - will be overriden to the old one if needed UpdateSpec.getRevision(primary).ifPresent(rev -> addUpdateRevisionAnnotation(rev, baseDeployment)); + addUpdateHashAnnotation(KeycloakUpdateJobDependentResource.keycloakHash(primary), baseDeployment); var existingDeployment = ContextUtils.getCurrentStatefulSet(context).orElse(null); @@ -164,6 +164,13 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent } baseDeployment.getSpec().setServiceName(serviceName); + return baseDeployment; + } + + @Override + public StatefulSet desired(Keycloak primary, Context context) { + StatefulSet baseDeployment = ContextUtils.getDesiredStatefulSet(context); + var existingDeployment = ContextUtils.getCurrentStatefulSet(context).orElse(null); var updateType = ContextUtils.getUpdateType(context); @@ -181,7 +188,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent return switch (updateType.get()) { case ROLLING -> handleRollingUpdate(baseDeployment); - case RECREATE -> handleRecreateUpdate(existingDeployment, baseDeployment, kcContainer); + case RECREATE -> handleRecreateUpdate(existingDeployment, baseDeployment, CRDUtils.firstContainerOf(baseDeployment).orElseThrow()); }; } @@ -627,8 +634,9 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent } else { Log.debug("Performing a recreate update - scaling down the stateful set"); - // keep the old revision and image, mark as migrating, and scale down - CRDUtils.getRevision(actual).ifPresent(rev -> addUpdateRevisionAnnotation(rev, desired)); + // keep the old revision, image, and hash, then mark as migrating, and scale down + addOrRemoveAnnotation(CRDUtils.getRevision(actual).orElse(null), Constants.KEYCLOAK_UPDATE_REVISION_ANNOTATION, desired); + addOrRemoveAnnotation(CRDUtils.getUpdateHash(actual).orElse(null), Constants.KEYCLOAK_UPDATE_HASH_ANNOTATION, desired); desired.getMetadata().getAnnotations().put(Constants.KEYCLOAK_MIGRATING_ANNOTATION, Boolean.TRUE.toString()); desired.getSpec().setReplicas(0); var currentImage = RecreateOnImageChangeUpdateLogic.extractImage(actual); @@ -641,6 +649,14 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent toUpdate.getMetadata().getAnnotations().put(Constants.KEYCLOAK_UPDATE_REVISION_ANNOTATION, revision); } + private static void addUpdateHashAnnotation(String hash, StatefulSet toUpdate) { + toUpdate.getMetadata().getAnnotations().put(Constants.KEYCLOAK_UPDATE_HASH_ANNOTATION, hash); + } + + private static void addOrRemoveAnnotation(String value, String annotation, StatefulSet toUpdate) { + toUpdate.getMetadata().getAnnotations().compute(annotation, (k, v) -> value); + } + record ManagementEndpoint(String relativePath, String protocol, int port, String portName) {} static ManagementEndpoint managementEndpoint(Keycloak keycloakCR, Context context, boolean health) { diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakUpdateJobDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakUpdateJobDependentResource.java index 1e044f3636f..3e52a2a10b2 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakUpdateJobDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakUpdateJobDependentResource.java @@ -224,7 +224,7 @@ public class KeycloakUpdateJobDependentResource extends CRUDKubernetesDependentR return Stream.concat(updateArgs.stream(), currentArgs.stream().filter(arg -> !arg.equals("start"))).toList(); } - static String keycloakHash(Keycloak keycloak) { + public static String keycloakHash(Keycloak keycloak) { return Utils.hash( List.of(new KeycloakSpecBuilder(keycloak.getSpec()).withInstances(null).withLivenessProbeSpec(null) .withStartupProbeSpec(null).withReadinessProbeSpec(null).withResourceRequirements(null) diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java index 0583c846c17..0f94bd5a05a 100644 --- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java +++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java @@ -123,4 +123,11 @@ public final class CRDUtils { .map(ObjectMeta::getAnnotations) .map(annotations -> annotations.get(Constants.KEYCLOAK_UPDATE_REVISION_ANNOTATION)); } + + public static Optional getUpdateHash(StatefulSet statefulSet) { + return Optional.ofNullable(statefulSet) + .map(StatefulSet::getMetadata) + .map(ObjectMeta::getAnnotations) + .map(annotations -> annotations.get(Constants.KEYCLOAK_UPDATE_HASH_ANNOTATION)); + } } diff --git a/operator/src/main/java/org/keycloak/operator/update/impl/BaseUpgradeLogic.java b/operator/src/main/java/org/keycloak/operator/update/impl/BaseUpdateLogic.java similarity index 95% rename from operator/src/main/java/org/keycloak/operator/update/impl/BaseUpgradeLogic.java rename to operator/src/main/java/org/keycloak/operator/update/impl/BaseUpdateLogic.java index 495849934cb..6c25fdd765d 100644 --- a/operator/src/main/java/org/keycloak/operator/update/impl/BaseUpgradeLogic.java +++ b/operator/src/main/java/org/keycloak/operator/update/impl/BaseUpdateLogic.java @@ -65,8 +65,15 @@ abstract class BaseUpdateLogic implements UpdateLogic { } copyStatusFromExistStatefulSet(existing.get()); + Optional storedHash = CRDUtils.getUpdateHash(existing.get()); var desiredStatefulSet = ContextUtils.getDesiredStatefulSet(context); var desiredContainer = CRDUtils.firstContainerOf(desiredStatefulSet).orElseThrow(BaseUpdateLogic::containerNotFound); + + if (Objects.equals(CRDUtils.getUpdateHash(desiredStatefulSet).orElseThrow(), storedHash.orElse(null))) { + Log.debug("Hash is equals - skipping update logic"); + return Optional.empty(); + } + var actualContainer = CRDUtils.firstContainerOf(existing.get()).orElseThrow(BaseUpdateLogic::containerNotFound); if (isContainerEquals(actualContainer, desiredContainer)) { 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 f856c97aba3..7e35a9b21b7 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 @@ -123,7 +123,7 @@ public class PodTemplateTest { //noinspection unchecked Context context = mockContext(null); - return deployment.desired(kc, context); + return deployment.initialDesired(kc, context); } private Keycloak createKeycloak(PodTemplateSpec podTemplate, Consumer additionalSpec) {