expands the status handling to account for the prior status (#20856)

Closes #20853
This commit is contained in:
Steven Hawkins 2023-06-08 11:09:39 -04:00 committed by GitHub
parent 61968bf747
commit 91a3ab6b87
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 300 additions and 53 deletions

View file

@ -19,6 +19,10 @@ package org.keycloak.operator;
import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClient;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
/** /**
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
@ -26,4 +30,13 @@ public final class Utils {
public static boolean isOpenShift(KubernetesClient client) { public static boolean isOpenShift(KubernetesClient client) {
return client.supports("operator.openshift.io/v1", "OpenShiftAPIServer"); return client.supports("operator.openshift.io/v1", "OpenShiftAPIServer");
} }
/**
* Returns the current timestamp in ISO 8601 format, for example "2019-07-23T09:08:12.356Z".
* @return the current timestamp in ISO 8601 format, for example "2019-07-23T09:08:12.356Z".
*/
public static String iso8601Now() {
return ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
}
} }

View file

@ -98,7 +98,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace); Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);
var statusAggregator = new KeycloakStatusAggregator(); var statusAggregator = new KeycloakStatusAggregator(kc.getStatus(), kc.getMetadata().getGeneration());
var kcAdminSecret = new KeycloakAdminSecret(client, kc); var kcAdminSecret = new KeycloakAdminSecret(client, kc);
kcAdminSecret.createOrUpdateReconciled(); kcAdminSecret.createOrUpdateReconciled();
@ -139,10 +139,8 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
updateControl = UpdateControl.updateStatus(kc); updateControl = UpdateControl.updateStatus(kc);
} }
if (status if (status.findCondition(KeycloakStatusCondition.READY)
.getConditions() .filter(c -> !Boolean.TRUE.equals(c.getStatus())).isPresent()) {
.stream()
.anyMatch(c -> c.getType().equals(KeycloakStatusCondition.READY) && !c.getStatus())) {
updateControl.rescheduleAfter(10, TimeUnit.SECONDS); updateControl.rescheduleAfter(10, TimeUnit.SECONDS);
} }
@ -152,7 +150,7 @@ public class KeycloakController implements Reconciler<Keycloak>, EventSourceInit
@Override @Override
public ErrorStatusUpdateControl<Keycloak> updateErrorStatus(Keycloak kc, Context<Keycloak> context, Exception e) { public ErrorStatusUpdateControl<Keycloak> updateErrorStatus(Keycloak kc, Context<Keycloak> context, Exception e) {
Log.error("--- Error reconciling", e); Log.error("--- Error reconciling", e);
KeycloakStatus status = new KeycloakStatusAggregator() KeycloakStatus status = new KeycloakStatusAggregator(kc.getStatus(), kc.getMetadata().getGeneration())
.addErrorMessage("Error performing operations:\n" + e.getMessage()) .addErrorMessage("Error performing operations:\n" + e.getMessage())
.build(); .build();

View file

@ -18,36 +18,39 @@ package org.keycloak.operator.crds.v2alpha1.deployment;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import io.fabric8.kubernetes.model.annotation.LabelSelector; import io.fabric8.kubernetes.model.annotation.LabelSelector;
import io.fabric8.kubernetes.model.annotation.StatusReplicas; import io.fabric8.kubernetes.model.annotation.StatusReplicas;
import io.javaoperatorsdk.operator.api.ObservedGenerationAware;
import io.sundr.builder.annotations.Buildable; import io.sundr.builder.annotations.Buildable;
/** /**
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder", lazyCollectionInitEnabled = false) @Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder", lazyCollectionInitEnabled = false)
public class KeycloakStatus { public class KeycloakStatus implements ObservedGenerationAware {
@LabelSelector @LabelSelector
private String selector; private String selector;
@StatusReplicas @StatusReplicas
private Integer instances; private Integer instances;
private Long observedGeneration;
private List<KeycloakStatusCondition> conditions; private List<KeycloakStatusCondition> conditions;
public String getSelector() { public String getSelector() {
return selector; return selector;
} }
public void setSelector(String selector) { public void setSelector(String selector) {
this.selector = selector; this.selector = selector;
} }
public Integer getInstances() { public Integer getInstances() {
return instances; return instances;
} }
public void setInstances(Integer instances) { public void setInstances(Integer instances) {
this.instances = instances; this.instances = instances;
} }
@ -60,18 +63,36 @@ public class KeycloakStatus {
this.conditions = conditions; this.conditions = conditions;
} }
public Optional<KeycloakStatusCondition> findCondition(String type) {
if (conditions == null || conditions.isEmpty()) {
return Optional.empty();
}
return conditions.stream().filter(c -> type.equals(c.getType())).findFirst();
}
@Override
public Long getObservedGeneration() {
return observedGeneration;
}
@Override
public void setObservedGeneration(Long generation) {
this.observedGeneration = generation;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
KeycloakStatus status = (KeycloakStatus) o; KeycloakStatus status = (KeycloakStatus) o;
return Objects.equals(getConditions(), status.getConditions()) return Objects.equals(getConditions(), status.getConditions())
&& Objects.equals(getInstances(), status.getInstances()) && Objects.equals(getInstances(), status.getInstances())
&& Objects.equals(getSelector(), status.getSelector()); && Objects.equals(getSelector(), status.getSelector())
&& Objects.equals(getObservedGeneration(), status.getObservedGeneration());
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(getConditions(), getInstances(), getSelector()); return Objects.hash(getConditions(), getInstances(), getSelector(), getObservedGeneration());
} }
} }

View file

@ -17,61 +17,90 @@
package org.keycloak.operator.crds.v2alpha1.deployment; package org.keycloak.operator.crds.v2alpha1.deployment;
import org.keycloak.operator.Utils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/** /**
* @author Vaclav Muzikar <vmuzikar@redhat.com> * @author Vaclav Muzikar <vmuzikar@redhat.com>
*/ */
public class KeycloakStatusAggregator { public class KeycloakStatusAggregator {
private final KeycloakStatusCondition readyCondition; private final KeycloakStatusCondition readyCondition = new KeycloakStatusCondition();
private final KeycloakStatusCondition hasErrorsCondition; private final KeycloakStatusCondition hasErrorsCondition = new KeycloakStatusCondition();
private final KeycloakStatusCondition rollingUpdate; private final KeycloakStatusCondition rollingUpdate = new KeycloakStatusCondition();
private final List<String> notReadyMessages = new ArrayList<>(); private final List<String> notReadyMessages = new ArrayList<>();
private final List<String> errorMessages = new ArrayList<>(); private final List<String> errorMessages = new ArrayList<>();
private final List<String> rollingUpdateMessages = new ArrayList<>(); private final List<String> rollingUpdateMessages = new ArrayList<>();
private final KeycloakStatusBuilder statusBuilder = new KeycloakStatusBuilder();
public KeycloakStatusAggregator() { private final KeycloakStatusBuilder statusBuilder;
readyCondition = new KeycloakStatusCondition(); private final Map<String, KeycloakStatusCondition> existingConditions;
private final Long observedGeneration;
/**
* @param generation the observedGeneration for conditions
*/
public KeycloakStatusAggregator(Long generation) {
this(null, generation);
}
/**
* @param current status, which is used as a base for the next conditions
* @param generation the observedGeneration for conditions
*/
public KeycloakStatusAggregator(KeycloakStatus current, Long generation) {
if (current != null) { // 6.7 fabric8 no longer requires this null check
statusBuilder = new KeycloakStatusBuilder(current);
existingConditions = Optional.ofNullable(current.getConditions()).orElse(List.of()).stream().collect(Collectors.toMap(KeycloakStatusCondition::getType, Function.identity()));
} else {
statusBuilder = new KeycloakStatusBuilder();
existingConditions = Map.of();
}
// we're not setting this on the statusBuilder as we're letting the sdk manage that
observedGeneration = generation;
readyCondition.setType(KeycloakStatusCondition.READY); readyCondition.setType(KeycloakStatusCondition.READY);
readyCondition.setStatus(true);
hasErrorsCondition = new KeycloakStatusCondition();
hasErrorsCondition.setType(KeycloakStatusCondition.HAS_ERRORS); hasErrorsCondition.setType(KeycloakStatusCondition.HAS_ERRORS);
hasErrorsCondition.setStatus(false);
rollingUpdate = new KeycloakStatusCondition();
rollingUpdate.setType(KeycloakStatusCondition.ROLLING_UPDATE); rollingUpdate.setType(KeycloakStatusCondition.ROLLING_UPDATE);
rollingUpdate.setStatus(false);
} }
public KeycloakStatusAggregator addNotReadyMessage(String message) { public KeycloakStatusAggregator addNotReadyMessage(String message) {
readyCondition.setStatus(false); readyCondition.setStatus(false);
readyCondition.setObservedGeneration(observedGeneration);
notReadyMessages.add(message); notReadyMessages.add(message);
return this; return this;
} }
public KeycloakStatusAggregator addErrorMessage(String message) { public KeycloakStatusAggregator addErrorMessage(String message) {
hasErrorsCondition.setStatus(true); hasErrorsCondition.setStatus(true);
hasErrorsCondition.setObservedGeneration(observedGeneration);
errorMessages.add(message); errorMessages.add(message);
return this; return this;
} }
public KeycloakStatusAggregator addWarningMessage(String message) { public KeycloakStatusAggregator addWarningMessage(String message) {
errorMessages.add("warning: " + message); errorMessages.add("warning: " + message);
hasErrorsCondition.setObservedGeneration(observedGeneration);
return this; return this;
} }
public KeycloakStatusAggregator addRollingUpdateMessage(String message) { public KeycloakStatusAggregator addRollingUpdateMessage(String message) {
rollingUpdate.setStatus(true); rollingUpdate.setStatus(true);
rollingUpdate.setObservedGeneration(observedGeneration);
rollingUpdateMessages.add(message); rollingUpdateMessages.add(message);
return this; return this;
} }
/** /**
* Apply non-condition changes to the status * Apply non-condition changes to the status
*/ */
@ -85,10 +114,58 @@ public class KeycloakStatusAggregator {
} }
public KeycloakStatus build() { public KeycloakStatus build() {
readyCondition.setMessage(String.join("\n", notReadyMessages)); // conditions are only updated in one direction - the following determines if it's appropriate to observe the default / other direction
hasErrorsCondition.setMessage(String.join("\n", errorMessages)); if (readyCondition.getStatus() == null && !Boolean.TRUE.equals(hasErrorsCondition.getStatus())) {
rollingUpdate.setMessage(String.join("\n", rollingUpdateMessages)); readyCondition.setStatus(true);
readyCondition.setObservedGeneration(observedGeneration);
}
if (readyCondition.getObservedGeneration() != null) {
readyCondition.setMessage(String.join("\n", notReadyMessages));
}
if (hasErrorsCondition.getStatus() == null && readyCondition.getObservedGeneration() != null) {
hasErrorsCondition.setStatus(false);
hasErrorsCondition.setObservedGeneration(observedGeneration);
}
if (hasErrorsCondition.getObservedGeneration() != null) {
hasErrorsCondition.setMessage(String.join("\n", errorMessages));
}
if (rollingUpdate.getStatus() == null && readyCondition.getObservedGeneration() != null) {
rollingUpdate.setStatus(false);
rollingUpdate.setObservedGeneration(observedGeneration);
}
if (rollingUpdate.getObservedGeneration() != null) {
rollingUpdate.setMessage(String.join("\n", rollingUpdateMessages));
}
String now = Utils.iso8601Now();
updateConditionFromExisting(readyCondition, existingConditions, now);
updateConditionFromExisting(hasErrorsCondition, existingConditions, now);
updateConditionFromExisting(rollingUpdate, existingConditions, now);
return statusBuilder.withConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate)).build(); return statusBuilder.withConditions(List.of(readyCondition, hasErrorsCondition, rollingUpdate)).build();
} }
static void updateConditionFromExisting(KeycloakStatusCondition condition, Map<String, KeycloakStatusCondition> existingConditions, String now) {
var existing = existingConditions.get(condition.getType());
if (existing == null) {
if (condition.getObservedGeneration() != null) {
condition.setLastTransitionTime(now);
}
} else if (condition.getObservedGeneration() == null) {
// carry the existing forward
condition.setLastTransitionTime(existing.getLastTransitionTime());
condition.setObservedGeneration(existing.getObservedGeneration());
condition.setStatus(existing.getStatus());
if (condition.getMessage() == null) {
condition.setMessage(existing.getMessage());
}
} else if (Objects.equals(existing.getStatus(), condition.getStatus())
&& Objects.equals(existing.getMessage(), condition.getMessage())) {
condition.setLastTransitionTime(existing.getLastTransitionTime());
} else {
condition.setLastTransitionTime(now);
}
}
} }

View file

@ -31,6 +31,8 @@ public class KeycloakStatusCondition {
private String type; private String type;
private Boolean status; private Boolean status;
private String message; private String message;
private String lastTransitionTime;
private Long observedGeneration;
public String getType() { public String getType() {
return type; return type;
@ -56,17 +58,35 @@ public class KeycloakStatusCondition {
this.message = message; this.message = message;
} }
public String getLastTransitionTime() {
return lastTransitionTime;
}
public void setLastTransitionTime(String lastTransitionTime) {
this.lastTransitionTime = lastTransitionTime;
}
public Long getObservedGeneration() {
return observedGeneration;
}
public void setObservedGeneration(Long observedGeneration) {
this.observedGeneration = observedGeneration;
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false; if (o == null || getClass() != o.getClass()) return false;
KeycloakStatusCondition that = (KeycloakStatusCondition) o; KeycloakStatusCondition that = (KeycloakStatusCondition) o;
return Objects.equals(getType(), that.getType()) && Objects.equals(getStatus(), that.getStatus()) && Objects.equals(getMessage(), that.getMessage()); return Objects.equals(getType(), that.getType()) && Objects.equals(getStatus(), that.getStatus()) && Objects.equals(getMessage(), that.getMessage())
&& Objects.equals(getLastTransitionTime(), that.getLastTransitionTime())
&& Objects.equals(getObservedGeneration(), that.getObservedGeneration());
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(getType(), getStatus(), getMessage()); return Objects.hash(getType(), getStatus(), getMessage(), getObservedGeneration(), getLastTransitionTime());
} }
@Override @Override

View file

@ -79,6 +79,10 @@ public class KeycloakDeploymentTest extends BaseOperatorTest {
Log.info("Checking Keycloak pod has ready replicas == 1"); Log.info("Checking Keycloak pod has ready replicas == 1");
assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName(deploymentName).get().getStatus().getReadyReplicas()).isEqualTo(1); assertThat(k8sclient.apps().statefulSets().inNamespace(namespace).withName(deploymentName).get().getStatus().getReadyReplicas()).isEqualTo(1);
Log.info("Checking observedGeneration is the same as the spec");
Keycloak latest = k8sclient.resource(kc).get();
assertThat(latest.getMetadata().getGeneration()).isEqualTo(latest.getStatus().getObservedGeneration());
// Delete CR // Delete CR
Log.info("Deleting Keycloak CR and watching cleanup"); Log.info("Deleting Keycloak CR and watching cleanup");
k8sclient.resource(kc).delete(); k8sclient.resource(kc).delete();

View file

@ -22,6 +22,7 @@ import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.apps.StatefulSet; import io.fabric8.kubernetes.api.model.apps.StatefulSet;
import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder;
import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.common.util.CollectionUtil; import org.keycloak.common.util.CollectionUtil;
import org.keycloak.operator.Constants; import org.keycloak.operator.Constants;
@ -208,7 +209,7 @@ public class KeycloakDistConfiguratorTest {
private void assertWarningStatusFirstClassFields(KeycloakDistConfigurator distConfig, boolean expectWarning, Collection<String> firstClassFields) { private void assertWarningStatusFirstClassFields(KeycloakDistConfigurator distConfig, boolean expectWarning, Collection<String> firstClassFields) {
final String message = "warning: You need to specify these fields as the first-class citizen of the CR: "; final String message = "warning: You need to specify these fields as the first-class citizen of the CR: ";
final KeycloakStatusAggregator statusBuilder = new KeycloakStatusAggregator(); final KeycloakStatusAggregator statusBuilder = new KeycloakStatusAggregator(1L);
distConfig.validateOptions(statusBuilder); distConfig.validateOptions(statusBuilder);
final KeycloakStatus status = statusBuilder.build(); final KeycloakStatus status = statusBuilder.build();

View file

@ -17,21 +17,124 @@
package org.keycloak.operator.testsuite.unit; package org.keycloak.operator.testsuite.unit;
import static org.junit.jupiter.api.Assertions.assertNotEquals; import org.assertj.core.api.Condition;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusAggregator;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
import org.keycloak.operator.testsuite.utils.CRAssert;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class KeycloakStatusTest { public class KeycloakStatusTest {
@Test @Test
public void testEqualityWithScale() { public void testEqualityWithScale() {
KeycloakStatus status1 = new KeycloakStatusAggregator().apply(b -> b.withInstances(1)).build(); KeycloakStatus status1 = new KeycloakStatusAggregator(0L).apply(b -> b.withInstances(1)).build();
KeycloakStatus status2 = new KeycloakStatusAggregator().apply(b -> b.withInstances(2)).build(); KeycloakStatus status2 = new KeycloakStatusAggregator(0L).apply(b -> b.withInstances(2)).build();
assertNotEquals(status1, status2); assertNotEquals(status1, status2);
} }
@Test
public void testDefaults() {
KeycloakStatus status = new KeycloakStatusAggregator(1L).build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, true, "", 1L);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.ROLLING_UPDATE, false, "", 1L);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, false, "", 1L);
}
@Test
public void testReadyWithWarning() {
KeycloakStatus status = new KeycloakStatusAggregator(1L).addWarningMessage("something's not right").build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, true, "", 1L);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, false, "warning: something's not right", 1L); // could also be unknown
}
@Test
public void testNotReady() {
KeycloakStatus status = new KeycloakStatusAggregator(1L).addNotReadyMessage("waiting").build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, false, "waiting", 1L);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, false, "", 1L);
}
@Test
public void testReadyRolling() {
KeycloakStatus status = new KeycloakStatusAggregator(1L).addRollingUpdateMessage("rolling").build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, true, "", 1L);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.ROLLING_UPDATE, true, "rolling", 1L);
}
@Test
public void testError() {
// without prior status, ready and rolling are unknown
KeycloakStatus status = new KeycloakStatusAggregator(1L).addErrorMessage("this is bad").build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, null, null, null);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, true, "this is bad", 1L);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.ROLLING_UPDATE, null, null, null);
}
@Test
public void testErrorWithPriorStatus() {
// with prior status, ready and rolling are preserved
KeycloakStatus prior = new KeycloakStatusAggregator(1L).build();
prior.getConditions().stream().forEach(c -> c.setLastTransitionTime("prior"));
KeycloakStatus status = new KeycloakStatusAggregator(prior, 2L).addErrorMessage("this is bad").build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, true, "", 1L)
.extracting(KeycloakStatusCondition::getLastTransitionTime).isEqualTo("prior");
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, true, "this is bad", 2L)
.extracting(KeycloakStatusCondition::getLastTransitionTime).isNotEqualTo("prior");
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.ROLLING_UPDATE, false, "", 1L);
}
@Test
public void testReadyWithPriorStatus() {
// without prior status, ready and rolling are known and keep the transition time
KeycloakStatus prior = new KeycloakStatusAggregator(1L).build();
prior.getConditions().stream().forEach(c -> c.setLastTransitionTime("prior"));
KeycloakStatus status = new KeycloakStatusAggregator(prior, 2L).build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, true, "", 2L)
.extracting(KeycloakStatusCondition::getLastTransitionTime).isEqualTo("prior");
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.HAS_ERRORS, false, "", 2L);
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.ROLLING_UPDATE, false, "", 2L);
}
@Test
public void testMessagesChangesLastTransitionTime() {
KeycloakStatus prior = new KeycloakStatusAggregator(1L).build();
prior.getConditions().stream().forEach(c -> {
c.setLastTransitionTime("prior");
c.setMessage("old");
});
KeycloakStatus status = new KeycloakStatusAggregator(prior, 2L).build();
CRAssert.assertKeycloakStatusCondition(status, KeycloakStatusCondition.READY, true, "", 2L).has(new Condition<>(
c -> !c.getLastTransitionTime().equals("prior") && !c.getMessage().equals("old"), "transitioned"));
}
@Test
public void testPreservesScale() {
KeycloakStatus prior = new KeycloakStatusAggregator(1L).apply(b -> b.withObservedGeneration(1L).withInstances(3)).build();
prior.getConditions().stream().forEach(c -> c.setLastTransitionTime("prior"));
KeycloakStatus status = new KeycloakStatusAggregator(prior, 2L).apply(b -> b.withObservedGeneration(2L)).build();
assertEquals(2, status.getObservedGeneration());
assertEquals(3, status.getInstances());
}
} }

View file

@ -19,8 +19,11 @@ package org.keycloak.operator.testsuite.utils;
import io.fabric8.kubernetes.client.utils.Serialization; import io.fabric8.kubernetes.client.utils.Serialization;
import io.quarkus.logging.Log; import io.quarkus.logging.Log;
import org.assertj.core.api.ObjectAssert;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus; import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatus;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport; import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -36,23 +39,30 @@ public final class CRAssert {
public static void assertKeycloakStatusCondition(Keycloak kc, String condition, boolean status, String containedMessage) { public static void assertKeycloakStatusCondition(Keycloak kc, String condition, boolean status, String containedMessage) {
Log.debugf("Asserting CR: %s, condition: %s, status: %s, message: %s", kc.getMetadata().getName(), condition, status, containedMessage); Log.debugf("Asserting CR: %s, condition: %s, status: %s, message: %s", kc.getMetadata().getName(), condition, status, containedMessage);
try { try {
assertKeycloakStatusCondition(kc.getStatus(), condition, status, containedMessage); assertKeycloakStatusCondition(kc.getStatus(), condition, status, containedMessage, null);
} catch (Exception e) { } catch (Exception e) {
Log.infof("Asserting CR: %s with status:\n%s", kc.getMetadata().getName(), Serialization.asYaml(kc.getStatus())); Log.infof("Asserting CR: %s with status:\n%s", kc.getMetadata().getName(), Serialization.asYaml(kc.getStatus()));
throw e; throw e;
} }
} }
public static void assertKeycloakStatusCondition(KeycloakStatus kcStatus, String condition, boolean status) { public static void assertKeycloakStatusCondition(KeycloakStatus kcStatus, String condition, Boolean status, String containedMessage) {
assertKeycloakStatusCondition(kcStatus, condition, status, null); assertKeycloakStatusCondition(kcStatus, condition, status, containedMessage, null);
} }
public static void assertKeycloakStatusCondition(KeycloakStatus kcStatus, String condition, boolean status, String containedMessage) {
assertThat(kcStatus.getConditions()) public static ObjectAssert<KeycloakStatusCondition> assertKeycloakStatusCondition(KeycloakStatus kcStatus, String condition, Boolean status, String containedMessage, Long observedGeneration) {
.anyMatch(c -> KeycloakStatusCondition statusCondition = kcStatus.findCondition(condition).orElseThrow();
c.getType().equals(condition) && assertThat(statusCondition.getStatus()).isEqualTo(status);
c.getStatus() == status && if (containedMessage != null) {
(containedMessage == null || c.getMessage().contains(containedMessage)) assertThat(statusCondition.getMessage()).contains(containedMessage);
); }
if (observedGeneration != null) {
assertThat(statusCondition.getObservedGeneration()).isEqualTo(observedGeneration);
}
if (status != null) {
assertThat(statusCondition.getLastTransitionTime()).isNotNull();
}
return assertThat(statusCondition);
} }
public static void assertKeycloakStatusDoesNotContainMessage(KeycloakStatus kcStatus, String message) { public static void assertKeycloakStatusDoesNotContainMessage(KeycloakStatus kcStatus, String message) {