diff --git a/operator/pom.xml b/operator/pom.xml index 0ca393166b..ee78be690b 100644 --- a/operator/pom.xml +++ b/operator/pom.xml @@ -40,13 +40,6 @@ 2.22.0 - - - s01.oss.sonatype - https://s01.oss.sonatype.org/content/repositories/snapshots/ - - - diff --git a/operator/src/main/java/org/keycloak/operator/OperatorManagedResource.java b/operator/src/main/java/org/keycloak/operator/OperatorManagedResource.java index fdb0ef8125..e2ec8586c2 100644 --- a/operator/src/main/java/org/keycloak/operator/OperatorManagedResource.java +++ b/operator/src/main/java/org/keycloak/operator/OperatorManagedResource.java @@ -22,6 +22,7 @@ import io.fabric8.kubernetes.api.model.OwnerReference; import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.Serialization; import io.quarkus.logging.Log; import java.util.Collections; @@ -43,16 +44,23 @@ public abstract class OperatorManagedResource { this.cr = cr; } - protected abstract HasMetadata getReconciledResource(); + protected abstract Optional getReconciledResource(); public void createOrUpdateReconciled() { - HasMetadata resource = getReconciledResource(); - setDefaultLabels(resource); - setOwnerReferences(resource); + getReconciledResource().ifPresent(resource -> { + try { + setDefaultLabels(resource); + setOwnerReferences(resource); - Log.debugf("Creating or updating resource: %s", resource); - resource = client.resource(resource).createOrReplace(); - Log.debugf("Successfully created or updated resource: %s", resource); + Log.debugf("Creating or updating resource: %s", resource); + resource = client.resource(resource).createOrReplace(); + Log.debugf("Successfully created or updated resource: %s", resource); + } catch (Exception e) { + Log.error("Failed to create or update resource"); + Log.error(Serialization.asYaml(resource)); + throw e; + } + }); } protected void setDefaultLabels(HasMetadata resource) { diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java index 4a5502b786..a63c61d121 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java @@ -61,7 +61,7 @@ public class KeycloakDeployment extends OperatorManagedResource { } @Override - protected HasMetadata getReconciledResource() { + protected Optional getReconciledResource() { Deployment baseDeployment = new DeploymentBuilder(this.baseDeployment).build(); // clone not to change the base template Deployment reconciledDeployment; if (existingDeployment == null) { @@ -75,7 +75,7 @@ public class KeycloakDeployment extends OperatorManagedResource { reconciledDeployment.setSpec(baseDeployment.getSpec()); } - return reconciledDeployment; + return Optional.of(reconciledDeployment); } private Deployment fetchExistingDeployment() { diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportController.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportController.java new file mode 100644 index 0000000000..085537313e --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportController.java @@ -0,0 +1,104 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.operator.v2alpha1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.fabric8.kubernetes.api.model.batch.v1.Job; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.informers.SharedIndexInformer; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.RetryInfo; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +import io.quarkus.logging.Log; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatus; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusBuilder; + +import javax.inject.Inject; + +import java.util.List; +import java.util.Optional; + +import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_FINALIZER; +import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE; + +@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE, finalizerName = NO_FINALIZER) +public class KeycloakRealmImportController implements Reconciler, EventSourceInitializer, ErrorStatusHandler { + + @Inject + KubernetesClient client; + + @Inject + ObjectMapper jsonMapper; + + @Override + public List prepareEventSources(EventSourceContext context) { + SharedIndexInformer jobInformer = + client.batch().v1().jobs().inNamespace(context.getConfigurationService().getClientConfiguration().getNamespace()) + .withLabels(org.keycloak.operator.Constants.DEFAULT_LABELS) + .runnableInformer(0); + + return List.of(new InformerEventSource<>(jobInformer, Mappers.fromOwnerReference())); + } + + @Override + public UpdateControl reconcile(KeycloakRealmImport realm, Context context) { + String realmName = realm.getMetadata().getName(); + String realmNamespace = realm.getMetadata().getNamespace(); + + Log.infof("--- Reconciling Keycloak Realm: %s in namespace: %s", realmName, realmNamespace); + + var statusBuilder = new KeycloakRealmImportStatusBuilder(); + + var realmImportSecret = new KeycloakRealmImportSecret(client, realm, jsonMapper); + realmImportSecret.createOrUpdateReconciled(); + + var realmImportJob = new KeycloakRealmImportJob(client, realm, realmImportSecret.getSecretName()); + realmImportJob.createOrUpdateReconciled(); + realmImportJob.updateStatus(statusBuilder); + + var status = statusBuilder.build(); + + Log.info("--- Realm reconciliation finished successfully"); + + if (status.equals(realm.getStatus())) { + return UpdateControl.noUpdate(); + } else { + realm.setStatus(status); + return UpdateControl.updateStatus(realm); + } + } + + @Override + public Optional updateErrorStatus(KeycloakRealmImport realm, RetryInfo retryInfo, RuntimeException e) { + Log.error("--- Error reconciling", e); + KeycloakRealmImportStatus status = new KeycloakRealmImportStatusBuilder() + .addErrorMessage("Error performing operations:\n" + e.getMessage()) + .build(); + + realm.setStatus(status); + return Optional.of(realm); + } +} diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportJob.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportJob.java new file mode 100644 index 0000000000..70d24d61bf --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportJob.java @@ -0,0 +1,213 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.operator.v2alpha1; + +import io.fabric8.kubernetes.api.model.Container; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder; +import io.fabric8.kubernetes.api.model.Volume; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMountBuilder; +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.fabric8.kubernetes.api.model.batch.v1.Job; +import io.fabric8.kubernetes.api.model.batch.v1.JobBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import io.quarkus.logging.Log; +import org.keycloak.operator.OperatorManagedResource; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusBuilder; + +import java.util.List; +import java.util.Optional; + +public class KeycloakRealmImportJob extends OperatorManagedResource { + + private final KeycloakRealmImport realmCR; + private final Deployment existingDeployment; + private final Job existingJob; + private final String secretName; + private final String volumeName; + + public KeycloakRealmImportJob(KubernetesClient client, KeycloakRealmImport realmCR, String secretName) { + super(client, realmCR); + this.realmCR = realmCR; + this.secretName = secretName; + this.volumeName = KubernetesResourceUtil.sanitizeName(secretName + "-volume"); + + this.existingJob = fetchExistingJob(); + this.existingDeployment = fetchExistingDeployment(); + } + + @Override + protected Optional getReconciledResource() { + if (existingJob == null) { + Log.info("Creating a new Job"); + return Optional.of(createImportJob()); + } else { + Log.info("Job already available"); + return Optional.empty(); + } + } + + private Job fetchExistingJob() { + return client + .batch() + .v1() + .jobs() + .inNamespace(getNamespace()) + .withName(getName()) + .get(); + } + + private Deployment fetchExistingDeployment() { + return client + .apps() + .deployments() + .inNamespace(getNamespace()) + .withName(getKeycloakName()) + .get(); + } + + private Job buildJob(Container keycloakContainer, Volume secretVolume) { + return new JobBuilder() + .withNewMetadata() + .withName(getName()) + .withNamespace(getNamespace()) + .endMetadata() + .withNewSpec() + .withNewTemplate() + .withNewSpec() + .withContainers(keycloakContainer) + .addToVolumes(secretVolume) + .withRestartPolicy("Never") + .endSpec() + .endTemplate() + .endSpec() + .build(); + } + + private Volume buildSecretVolume() { + return new VolumeBuilder() + .withName(volumeName) + .withSecret(new SecretVolumeSourceBuilder() + .withSecretName(secretName) + .build()) + .build(); + } + + private Job createImportJob() { + var keycloakContainer = buildKeycloakJobContainer(); + var secretVolume = buildSecretVolume(); + var importJob = buildJob(keycloakContainer, secretVolume); + + return importJob; + } + + private Container buildKeycloakJobContainer() { + var keycloakContainer = + this + .existingDeployment + .getSpec() + .getTemplate() + .getSpec() + .getContainers() + .get(0); + + var importMntPath = "/mnt/realm-import/"; + + var command = List.of("/bin/bash"); + + var override = "--override=false"; + + var commandArgs = List.of("-c", + "/opt/keycloak/bin/kc.sh build && " + + "/opt/keycloak/bin/kc.sh import --file='" + importMntPath + getRealmName() + "-realm.json' " + override); + + keycloakContainer + .setCommand(command); + keycloakContainer + .setArgs(commandArgs); + var volumeMounts = List.of( + new VolumeMountBuilder() + .withName(volumeName) + .withReadOnly(true) + .withMountPath(importMntPath) + .build()); + + keycloakContainer.setVolumeMounts(volumeMounts); + + // Disable probes since we are not really starting the server + keycloakContainer.setReadinessProbe(null); + keycloakContainer.setLivenessProbe(null); + + return keycloakContainer; + } + + + public void updateStatus(KeycloakRealmImportStatusBuilder status) { + if (existingDeployment == null) { + status.addNotReadyMessage("No existing Deployment found, waiting for it to be created"); + return; + } + + if (existingJob == null) { + Log.info("Job about to start"); + status.addStartedMessage("Import Job will start soon"); + } else { + Log.info("Job already executed - not recreating"); + var oldStatus = existingJob.getStatus(); + var lastReportedStatus = realmCR.getStatus(); + + if (oldStatus == null) { + Log.info("Job started"); + status.addStartedMessage("Import Job started"); + } else if (oldStatus.getSucceeded() != null && oldStatus.getSucceeded() > 0) { + if (!lastReportedStatus.isDone()) { + Log.info("Job finished performing a rolling restart of the deployment"); + rollingRestart(); + } + status.addDone(); + } else if (oldStatus.getFailed() != null && oldStatus.getFailed() > 0) { + Log.info("Job Failed"); + status.addErrorMessage("Import Job failed"); + } else { + Log.info("Job running"); + status.addStartedMessage("Import Job running"); + } + } + } + + private String getName() { + return realmCR.getMetadata().getName(); + } + + private String getNamespace() { + return realmCR.getMetadata().getNamespace(); + } + + private String getKeycloakName() { return realmCR.getSpec().getKeycloakCRName(); } + + private String getRealmName() { return realmCR.getSpec().getRealm().getRealm(); } + + private void rollingRestart() { + client.apps().deployments() + .inNamespace(getNamespace()) + .withName(getKeycloakName()) + .rolling().restart(); + } +} diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportSecret.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportSecret.java new file mode 100644 index 0000000000..61477344a1 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakRealmImportSecret.java @@ -0,0 +1,64 @@ +package org.keycloak.operator.v2alpha1; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.SecretBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.utils.KubernetesResourceUtil; +import org.keycloak.operator.OperatorManagedResource; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport; + +import java.util.Optional; + +public class KeycloakRealmImportSecret extends OperatorManagedResource { + + private final KeycloakRealmImport realmCR; + private final String secretName; + private final ObjectMapper jsonMapper; + + public KeycloakRealmImportSecret(KubernetesClient client, KeycloakRealmImport realmCR, ObjectMapper jsonMapper) { + super(client, realmCR); + this.realmCR = realmCR; + this.jsonMapper = jsonMapper; + this.secretName = KubernetesResourceUtil.sanitizeName(getName() + "-" + realmCR.getSpec().getRealm().getRealm() + "-realm"); + } + + @Override + protected Optional getReconciledResource() { + return Optional.of(createSecret()); + } + + private Secret createSecret() { + var fileName = getRealmName() + "-realm.json"; + var content = ""; + try { + content = jsonMapper.writeValueAsString(this.realmCR.getSpec().getRealm()); + } catch (JsonProcessingException cause) { + throw new RuntimeException("Failed to read the Realm Representation", cause); + } + + return new SecretBuilder() + .withNewMetadata() + .withName(secretName) + .withNamespace(getNamespace()) + .endMetadata() + .addToStringData(fileName, content) + .build(); + } + + private String getName() { + return realmCR.getMetadata().getName(); + } + + private String getNamespace() { + return realmCR.getMetadata().getNamespace(); + } + + private String getRealmName() { return realmCR.getSpec().getRealm().getRealm(); } + + public String getSecretName() { + return secretName; + } +} diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/Realm.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImport.java similarity index 62% rename from operator/src/main/java/org/keycloak/operator/v2alpha1/crds/Realm.java rename to operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImport.java index b668961b68..7cb302874b 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/Realm.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImport.java @@ -19,13 +19,19 @@ package org.keycloak.operator.v2alpha1.crds; import io.fabric8.kubernetes.api.model.Namespaced; import io.fabric8.kubernetes.client.CustomResource; import io.fabric8.kubernetes.model.annotation.Group; -import io.fabric8.kubernetes.model.annotation.Plural; -import io.fabric8.kubernetes.model.annotation.ShortNames; import io.fabric8.kubernetes.model.annotation.Version; +import io.sundr.builder.annotations.Buildable; +import io.sundr.builder.annotations.BuildableReference; import org.keycloak.operator.Constants; @Group(Constants.CRDS_GROUP) @Version(Constants.CRDS_VERSION) -public class Realm extends CustomResource implements Namespaced { +@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder", + lazyCollectionInitEnabled = false, refs = { + @BuildableReference(io.fabric8.kubernetes.api.model.ObjectMeta.class), + @BuildableReference(io.fabric8.kubernetes.client.CustomResource.class), + @BuildableReference(org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportSpec.class) +}) +public class KeycloakRealmImport extends CustomResource implements Namespaced { } diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/RealmSpec.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportSpec.java similarity index 82% rename from operator/src/main/java/org/keycloak/operator/v2alpha1/crds/RealmSpec.java rename to operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportSpec.java index f8d658236e..363afee3b0 100644 --- a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/RealmSpec.java +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportSpec.java @@ -16,15 +16,18 @@ */ package org.keycloak.operator.v2alpha1.crds; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; import org.keycloak.representations.idm.RealmRepresentation; import javax.validation.constraints.NotNull; -public class RealmSpec { +public class KeycloakRealmImportSpec { @NotNull + @JsonPropertyDescription("The name of the Keycloak CR to reference, in the same namespace.") private String keycloakCRName; @NotNull + @JsonPropertyDescription("The RealmRepresentation to import into Keycloak.") private RealmRepresentation realm; public String getKeycloakCRName() { diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatus.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatus.java new file mode 100644 index 0000000000..fb81441f25 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatus.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.operator.v2alpha1.crds; + +import java.util.List; +import java.util.Objects; + +import static org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition.DONE; + +public class KeycloakRealmImportStatus { + private List conditions; + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } + + public boolean isDone() { + return conditions + .stream() + .anyMatch(c -> c.getStatus() && c.getType().equals(DONE)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeycloakRealmImportStatus status = (KeycloakRealmImportStatus) o; + return Objects.equals(getConditions(), status.getConditions()); + } + + @Override + public int hashCode() { + return Objects.hash(getConditions()); + } +} diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatusBuilder.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatusBuilder.java new file mode 100644 index 0000000000..cc7f8ff841 --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatusBuilder.java @@ -0,0 +1,86 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.operator.v2alpha1.crds; + +import java.util.ArrayList; +import java.util.List; + +public class KeycloakRealmImportStatusBuilder { + private final KeycloakRealmImportStatusCondition readyCondition; + private final KeycloakRealmImportStatusCondition startedCondition; + private final KeycloakRealmImportStatusCondition hasErrorsCondition; + + private final List notReadyMessages = new ArrayList<>(); + private final List startedMessages = new ArrayList<>(); + private final List errorMessages = new ArrayList<>(); + + public KeycloakRealmImportStatusBuilder() { + readyCondition = new KeycloakRealmImportStatusCondition(); + readyCondition.setType(KeycloakRealmImportStatusCondition.DONE); + readyCondition.setStatus(false); + + startedCondition = new KeycloakRealmImportStatusCondition(); + startedCondition.setType(KeycloakRealmImportStatusCondition.STARTED); + startedCondition.setStatus(false); + + hasErrorsCondition = new KeycloakRealmImportStatusCondition(); + hasErrorsCondition.setType(KeycloakRealmImportStatusCondition.HAS_ERRORS); + hasErrorsCondition.setStatus(false); + } + + public KeycloakRealmImportStatusBuilder addStartedMessage(String message) { + startedCondition.setStatus(true); + readyCondition.setStatus(false); + hasErrorsCondition.setStatus(false); + startedMessages.add(message); + return this; + } + + public KeycloakRealmImportStatusBuilder addDone() { + startedCondition.setStatus(false); + readyCondition.setStatus(true); + hasErrorsCondition.setStatus(false); + return this; + } + + public KeycloakRealmImportStatusBuilder addNotReadyMessage(String message) { + startedCondition.setStatus(false); + readyCondition.setStatus(false); + hasErrorsCondition.setStatus(false); + notReadyMessages.add(message); + return this; + } + + public KeycloakRealmImportStatusBuilder addErrorMessage(String message) { + startedCondition.setStatus(false); + readyCondition.setStatus(false); + hasErrorsCondition.setStatus(true); + errorMessages.add(message); + return this; + } + + public KeycloakRealmImportStatus build() { + readyCondition.setMessage(String.join("\n", notReadyMessages)); + startedCondition.setMessage(String.join("\n", startedMessages)); + hasErrorsCondition.setMessage(String.join("\n", errorMessages)); + + KeycloakRealmImportStatus status = new KeycloakRealmImportStatus(); + status.setConditions(List.of(readyCondition, startedCondition, hasErrorsCondition)); + return status; + } +} diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatusCondition.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatusCondition.java new file mode 100644 index 0000000000..5691e2df9d --- /dev/null +++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakRealmImportStatusCondition.java @@ -0,0 +1,68 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.operator.v2alpha1.crds; + +import java.util.Objects; + +public class KeycloakRealmImportStatusCondition { + public static final String DONE = "Done"; + public static final String STARTED = "Started"; + public static final String HAS_ERRORS = "HasErrors"; + + // string to avoid enums in CRDs + private String type; + private Boolean status; + private String message; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Boolean getStatus() { + return status; + } + + public void setStatus(Boolean status) { + this.status = status; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + KeycloakRealmImportStatusCondition that = (KeycloakRealmImportStatusCondition) o; + return getType() == that.getType() && Objects.equals(getStatus(), that.getStatus()) && Objects.equals(getMessage(), that.getMessage()); + } + + @Override + public int hashCode() { + return Objects.hash(getType(), getStatus(), getMessage()); + } +} diff --git a/operator/src/main/kubernetes/kubernetes.yml b/operator/src/main/kubernetes/kubernetes.yml index ed480562b9..76a83dbf64 100644 --- a/operator/src/main/kubernetes/kubernetes.yml +++ b/operator/src/main/kubernetes/kubernetes.yml @@ -15,6 +15,30 @@ rules: - delete - patch - update + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch + - create + - delete + - patch + - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/operator/src/main/resources/application.properties b/operator/src/main/resources/application.properties index c023295de6..db36f07018 100644 --- a/operator/src/main/resources/application.properties +++ b/operator/src/main/resources/application.properties @@ -1,5 +1,6 @@ quarkus.operator-sdk.crd.apply=true -quarkus.operator-sdk.generate-csv=true +# Disabled until this is fixed: https://github.com/quarkiverse/quarkus-operator-sdk/issues/213 +quarkus.operator-sdk.generate-csv=false quarkus.container-image.builder=jib quarkus.operator-sdk.crd.validate=false diff --git a/operator/src/main/resources/base-keycloak-deployment.yaml b/operator/src/main/resources/base-keycloak-deployment.yaml index 34ac757c70..175b8475da 100644 --- a/operator/src/main/resources/base-keycloak-deployment.yaml +++ b/operator/src/main/resources/base-keycloak-deployment.yaml @@ -32,14 +32,14 @@ spec: port: 8080 initialDelaySeconds: 15 periodSeconds: 2 - failureThreshold: 10 + failureThreshold: 100 readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 15 periodSeconds: 2 - failureThreshold: 10 + failureThreshold: 200 dnsPolicy: ClusterFirst restartPolicy: Always terminationGracePeriodSeconds: 30 diff --git a/operator/src/main/resources/example-realm.yaml b/operator/src/main/resources/example-realm.yaml new file mode 100644 index 0000000000..a07189fb1a --- /dev/null +++ b/operator/src/main/resources/example-realm.yaml @@ -0,0 +1,1848 @@ +apiVersion: keycloak.org/v2alpha1 +kind: KeycloakRealmImport +metadata: + name: example-count0-kc +spec: + keycloakCRName: example-kc + realm: + id: count0 + realm: count0 + notBefore: 0 + defaultSignatureAlgorithm: RS256 + revokeRefreshToken: false + refreshTokenMaxReuse: 0 + accessTokenLifespan: 300 + accessTokenLifespanForImplicitFlow: 900 + ssoSessionIdleTimeout: 1800 + ssoSessionMaxLifespan: 36000 + ssoSessionIdleTimeoutRememberMe: 0 + ssoSessionMaxLifespanRememberMe: 0 + offlineSessionIdleTimeout: 3000 + offlineSessionMaxLifespanEnabled: false + offlineSessionMaxLifespan: 5184000 + clientSessionIdleTimeout: 0 + clientSessionMaxLifespan: 0 + clientOfflineSessionIdleTimeout: 0 + clientOfflineSessionMaxLifespan: 0 + accessCodeLifespan: 60 + accessCodeLifespanUserAction: 300 + accessCodeLifespanLogin: 1800 + actionTokenGeneratedByAdminLifespan: 43200 + actionTokenGeneratedByUserLifespan: 300 + oauth2DeviceCodeLifespan: 600 + oauth2DevicePollingInterval: 5 + enabled: true + sslRequired: external + registrationAllowed: true + registrationEmailAsUsername: false + rememberMe: false + verifyEmail: false + loginWithEmailAllowed: true + duplicateEmailsAllowed: false + resetPasswordAllowed: false + editUsernameAllowed: false + bruteForceProtected: false + permanentLockout: false + maxFailureWaitSeconds: 900 + minimumQuickLoginWaitSeconds: 60 + waitIncrementSeconds: 60 + quickLoginCheckMilliSeconds: 1000 + maxDeltaTimeSeconds: 43200 + failureFactor: 30 + roles: + realm: + - id: c118f6c0-db44-4b29-a439-573b0d828e61 + name: count0 + composite: false + clientRole: false + containerId: count0 + attributes: {} + - id: 999fa353-a573-4a20-b8b0-07d7e52faf85 + name: default-roles-count0 + description: "${role_default-roles}" + composite: true + composites: + realm: + - offline_access + - uma_authorization + client: + account: + - view-profile + - manage-account + clientRole: false + containerId: count0 + attributes: {} + - id: 62564c32-9ede-401c-9539-b12161c61b9e + name: offline_access + description: "${role_offline-access}" + composite: false + clientRole: false + containerId: count0 + attributes: {} + - id: 73322596-197c-4dd6-b15c-e60ee2ae2bf2 + name: count1 + composite: false + clientRole: false + containerId: count0 + attributes: {} + - id: 0aa06753-f4f6-471a-b6c2-90ab65c960fe + name: count2 + composite: false + clientRole: false + containerId: count0 + attributes: {} + - id: bcc954ae-9cae-4e65-8044-757178afb8e7 + name: uma_authorization + description: "${role_uma_authorization}" + composite: false + clientRole: false + containerId: count0 + attributes: {} + client: + count1: + - id: dc85702e-7b9a-4fe3-b508-ba6c2911a553 + name: count1-count1 + composite: false + clientRole: true + containerId: 814dc112-4eaa-4d79-b67d-c56ec58b667d + attributes: {} + - id: 8ca90cc8-5846-4af3-8d67-59637b60aa67 + name: count1-count2 + composite: false + clientRole: true + containerId: 814dc112-4eaa-4d79-b67d-c56ec58b667d + attributes: {} + - id: 026cc9d9-8bec-4598-89b9-07e5cac2d261 + name: count1-count0 + composite: false + clientRole: true + containerId: 814dc112-4eaa-4d79-b67d-c56ec58b667d + attributes: {} + count2: + - id: 9b30a355-c544-45f5-8b4d-77c797c518ad + name: count2-count1 + composite: false + clientRole: true + containerId: 363a2d11-f108-4601-ac99-1492326fb965 + attributes: {} + - id: 96c4cf02-60ec-469b-8fb0-cfbd2cdcd668 + name: count2-count0 + composite: false + clientRole: true + containerId: 363a2d11-f108-4601-ac99-1492326fb965 + attributes: {} + - id: e154dc95-c90b-446a-b8a2-ec2acea2b1fa + name: count2-count2 + composite: false + clientRole: true + containerId: 363a2d11-f108-4601-ac99-1492326fb965 + attributes: {} + realm-management: + - id: 5b2334dd-fb70-4454-ad6a-9ff9922d05a3 + name: manage-users + description: "${role_manage-users}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: d2a8141c-bc34-4091-b06d-ae5fe89e7c95 + name: impersonation + description: "${role_impersonation}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 480cc091-2ea3-47d9-ac1b-d4b23bceaaf3 + name: query-users + description: "${role_query-users}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 55407170-0249-4528-9754-7b2ed0a7e66d + name: view-events + description: "${role_view-events}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 4b3ab5d8-f6d8-4e2c-a8f8-73288fd795cd + name: view-realm + description: "${role_view-realm}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: f891606c-53ca-4016-ac1d-63db511920a3 + name: realm-admin + description: "${role_realm-admin}" + composite: true + composites: + client: + realm-management: + - manage-users + - query-users + - impersonation + - view-events + - view-realm + - query-clients + - view-authorization + - view-clients + - manage-authorization + - view-identity-providers + - query-groups + - manage-identity-providers + - manage-events + - manage-realm + - query-realms + - create-client + - manage-clients + - view-users + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 364de0ba-8c23-4f3a-a976-baebe67ed214 + name: query-clients + description: "${role_query-clients}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 49ffec23-bf9e-42b2-8056-0215e77076d1 + name: view-authorization + description: "${role_view-authorization}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 68330c4e-3728-4886-8fb4-f2367b018aa3 + name: manage-authorization + description: "${role_manage-authorization}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 41efa448-9770-4e61-a544-a3ff8691cd57 + name: view-clients + description: "${role_view-clients}" + composite: true + composites: + client: + realm-management: + - query-clients + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 7fdcbae6-d073-4ead-a7ec-091d2d84ea4a + name: view-identity-providers + description: "${role_view-identity-providers}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 7b890fde-b854-4d90-baf0-5b9c9e0b4ea6 + name: manage-identity-providers + description: "${role_manage-identity-providers}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 4adeb720-65b2-4bb2-bfd5-82e10cc09f8e + name: query-groups + description: "${role_query-groups}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 52d2867c-ef0d-48d9-81b4-89a9e0f986df + name: manage-events + description: "${role_manage-events}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 67d3f7db-131c-44df-ad5a-6b41eaecb835 + name: manage-realm + description: "${role_manage-realm}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: cbcbcc57-9742-47cb-910b-d795df46327b + name: query-realms + description: "${role_query-realms}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 74ff0c3a-90cd-4ad2-8c6e-f024d40d5f0a + name: create-client + description: "${role_create-client}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: 7e884119-1623-4b56-ae72-e33941f30a46 + name: manage-clients + description: "${role_manage-clients}" + composite: false + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + - id: a0ef6938-57f1-46bd-bf45-b4eb0ee14723 + name: view-users + description: "${role_view-users}" + composite: true + composites: + client: + realm-management: + - query-users + - query-groups + clientRole: true + containerId: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + attributes: {} + count0: + - id: 44e64e53-4bb2-4b51-93f4-7df74ad22168 + name: count0-count0 + composite: false + clientRole: true + containerId: 06ff4737-f005-495a-8755-4e7bcdffbc30 + attributes: {} + - id: 41d429f0-0993-4f00-bf29-8799ddd6af13 + name: count0-count2 + composite: false + clientRole: true + containerId: 06ff4737-f005-495a-8755-4e7bcdffbc30 + attributes: {} + - id: 522ffb44-d76a-4118-9d95-a99e4a6cd4af + name: count0-count1 + composite: false + clientRole: true + containerId: 06ff4737-f005-495a-8755-4e7bcdffbc30 + attributes: {} + security-admin-console: [] + admin-cli: [] + account-console: [] + broker: + - id: 77536924-22e3-4f93-9949-e684f5f9df6e + name: read-token + description: "${role_read-token}" + composite: false + clientRole: true + containerId: 18730050-7e05-432c-93e1-cd758ae6a776 + attributes: {} + account: + - id: 052ec680-28fe-45c6-9013-dd3151cdedc8 + name: view-profile + description: "${role_view-profile}" + composite: false + clientRole: true + containerId: a3fa25e9-f927-436e-b4ff-32926fd776be + attributes: {} + - id: 2416518b-f8db-4b7c-a3d5-d97d8a8bb932 + name: manage-account-links + description: "${role_manage-account-links}" + composite: false + clientRole: true + containerId: a3fa25e9-f927-436e-b4ff-32926fd776be + attributes: {} + - id: 8b1b17bf-97c7-427a-88f2-9dc9198beb8e + name: view-applications + description: "${role_view-applications}" + composite: false + clientRole: true + containerId: a3fa25e9-f927-436e-b4ff-32926fd776be + attributes: {} + - id: 9eef4927-3d35-49de-97c4-93a6c9af0171 + name: view-consent + description: "${role_view-consent}" + composite: false + clientRole: true + containerId: a3fa25e9-f927-436e-b4ff-32926fd776be + attributes: {} + - id: ff51791a-0dd9-4d97-90e6-9cb9ad2f4ee2 + name: delete-account + description: "${role_delete-account}" + composite: false + clientRole: true + containerId: a3fa25e9-f927-436e-b4ff-32926fd776be + attributes: {} + - id: a3314060-34e6-4596-81f3-f21d81fa8877 + name: manage-consent + description: "${role_manage-consent}" + composite: true + composites: + client: + account: + - view-consent + clientRole: true + containerId: a3fa25e9-f927-436e-b4ff-32926fd776be + attributes: {} + - id: c2ccc00f-02be-46d5-b52e-6d26ef823615 + name: manage-account + description: "${role_manage-account}" + composite: true + composites: + client: + account: + - manage-account-links + clientRole: true + containerId: a3fa25e9-f927-436e-b4ff-32926fd776be + attributes: {} + groups: + - id: 1f433252-3f96-44a2-95b4-db3ee2c4e224 + name: count0 + path: "/count0" + attributes: {} + realmRoles: [] + clientRoles: {} + subGroups: [] + - id: afd4225b-1982-478b-a3ec-0a29ba8e127e + name: count1 + path: "/count1" + attributes: {} + realmRoles: [] + clientRoles: {} + subGroups: [] + - id: 3993c319-c7a1-4bd0-b4cc-353ba7318e33 + name: count2 + path: "/count2" + attributes: {} + realmRoles: [] + clientRoles: {} + subGroups: [] + defaultRole: + id: 999fa353-a573-4a20-b8b0-07d7e52faf85 + name: default-roles-count0 + description: "${role_default-roles}" + composite: true + clientRole: false + containerId: count0 + requiredCredentials: + - password + passwordPolicy: hashIterations(3) + otpPolicyType: totp + otpPolicyAlgorithm: HmacSHA1 + otpPolicyInitialCounter: 0 + otpPolicyDigits: 6 + otpPolicyLookAheadWindow: 1 + otpPolicyPeriod: 30 + otpSupportedApplications: + - FreeOTP + - Google Authenticator + webAuthnPolicyRpEntityName: keycloak + webAuthnPolicySignatureAlgorithms: + - ES256 + webAuthnPolicyRpId: '' + webAuthnPolicyAttestationConveyancePreference: not specified + webAuthnPolicyAuthenticatorAttachment: not specified + webAuthnPolicyRequireResidentKey: not specified + webAuthnPolicyUserVerificationRequirement: not specified + webAuthnPolicyCreateTimeout: 0 + webAuthnPolicyAvoidSameAuthenticatorRegister: false + webAuthnPolicyAcceptableAaguids: [] + webAuthnPolicyPasswordlessRpEntityName: keycloak + webAuthnPolicyPasswordlessSignatureAlgorithms: + - ES256 + webAuthnPolicyPasswordlessRpId: '' + webAuthnPolicyPasswordlessAttestationConveyancePreference: not specified + webAuthnPolicyPasswordlessAuthenticatorAttachment: not specified + webAuthnPolicyPasswordlessRequireResidentKey: not specified + webAuthnPolicyPasswordlessUserVerificationRequirement: not specified + webAuthnPolicyPasswordlessCreateTimeout: 0 + webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister: false + webAuthnPolicyPasswordlessAcceptableAaguids: [] + scopeMappings: + - clientScope: offline_access + roles: + - offline_access + clientScopeMappings: + account: + - client: account-console + roles: + - manage-account + clients: + - id: a3fa25e9-f927-436e-b4ff-32926fd776be + clientId: account + name: "${client_account}" + rootUrl: "${authBaseUrl}" + baseUrl: "/realms/count0/account/" + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + redirectUris: + - "/realms/count0/account/*" + webOrigins: [] + notBefore: 0 + bearerOnly: false + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: false + serviceAccountsEnabled: false + publicClient: true + frontchannelLogout: false + protocol: openid-connect + attributes: {} + authenticationFlowBindingOverrides: {} + fullScopeAllowed: false + nodeReRegistrationTimeout: 0 + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: 70e036ed-30f1-4a32-bf05-582fe24baa76 + clientId: account-console + name: "${client_account-console}" + rootUrl: "${authBaseUrl}" + baseUrl: "/realms/count0/account/" + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + redirectUris: + - "/realms/count0/account/*" + webOrigins: [] + notBefore: 0 + bearerOnly: false + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: false + serviceAccountsEnabled: false + publicClient: true + frontchannelLogout: false + protocol: openid-connect + attributes: + pkce.code.challenge.method: S256 + authenticationFlowBindingOverrides: {} + fullScopeAllowed: false + nodeReRegistrationTimeout: 0 + protocolMappers: + - id: 2ae09f01-7ec3-4cef-ac18-81c4749ae4c6 + name: audience resolve + protocol: openid-connect + protocolMapper: oidc-audience-resolve-mapper + consentRequired: false + config: {} + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: 00f48072-5b8b-4e50-b97b-e2dcacabd753 + clientId: admin-cli + name: "${client_admin-cli}" + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + redirectUris: [] + webOrigins: [] + notBefore: 0 + bearerOnly: false + consentRequired: false + standardFlowEnabled: false + implicitFlowEnabled: false + directAccessGrantsEnabled: true + serviceAccountsEnabled: false + publicClient: true + frontchannelLogout: false + protocol: openid-connect + attributes: {} + authenticationFlowBindingOverrides: {} + fullScopeAllowed: false + nodeReRegistrationTimeout: 0 + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: 18730050-7e05-432c-93e1-cd758ae6a776 + clientId: broker + name: "${client_broker}" + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + redirectUris: [] + webOrigins: [] + notBefore: 0 + bearerOnly: true + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: false + serviceAccountsEnabled: false + publicClient: false + frontchannelLogout: false + protocol: openid-connect + attributes: {} + authenticationFlowBindingOverrides: {} + fullScopeAllowed: false + nodeReRegistrationTimeout: 0 + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: 06ff4737-f005-495a-8755-4e7bcdffbc30 + clientId: count0 + name: count0 + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + secret: count0-secret + redirectUris: + - "*" + webOrigins: [] + notBefore: 0 + bearerOnly: false + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: true + serviceAccountsEnabled: false + publicClient: false + frontchannelLogout: false + protocol: openid-connect + attributes: + backchannel.logout.session.required: 'true' + backchannel.logout.revoke.offline.tokens: 'false' + authenticationFlowBindingOverrides: {} + fullScopeAllowed: true + nodeReRegistrationTimeout: -1 + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: 814dc112-4eaa-4d79-b67d-c56ec58b667d + clientId: count1 + name: count1 + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + secret: count1-secret + redirectUris: + - "*" + webOrigins: [] + notBefore: 0 + bearerOnly: false + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: true + serviceAccountsEnabled: false + publicClient: false + frontchannelLogout: false + protocol: openid-connect + attributes: + backchannel.logout.session.required: 'true' + backchannel.logout.revoke.offline.tokens: 'false' + authenticationFlowBindingOverrides: {} + fullScopeAllowed: true + nodeReRegistrationTimeout: -1 + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: 363a2d11-f108-4601-ac99-1492326fb965 + clientId: count2 + name: count2 + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + secret: count2-secret + redirectUris: + - "*" + webOrigins: [] + notBefore: 0 + bearerOnly: false + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: true + serviceAccountsEnabled: false + publicClient: false + frontchannelLogout: false + protocol: openid-connect + attributes: + backchannel.logout.session.required: 'true' + backchannel.logout.revoke.offline.tokens: 'false' + authenticationFlowBindingOverrides: {} + fullScopeAllowed: true + nodeReRegistrationTimeout: -1 + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: a3890d4c-f2ba-41e9-a0a2-ab644681efa6 + clientId: realm-management + name: "${client_realm-management}" + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + redirectUris: [] + webOrigins: [] + notBefore: 0 + bearerOnly: true + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: false + serviceAccountsEnabled: false + publicClient: false + frontchannelLogout: false + protocol: openid-connect + attributes: {} + authenticationFlowBindingOverrides: {} + fullScopeAllowed: false + nodeReRegistrationTimeout: 0 + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + - id: e267ec9d-feef-427b-85e0-04005e833862 + clientId: security-admin-console + name: "${client_security-admin-console}" + rootUrl: "${authAdminUrl}" + baseUrl: "/admin/count0/console/" + surrogateAuthRequired: false + enabled: true + alwaysDisplayInConsole: false + clientAuthenticatorType: client-secret + redirectUris: + - "/admin/count0/console/*" + webOrigins: + - "+" + notBefore: 0 + bearerOnly: false + consentRequired: false + standardFlowEnabled: true + implicitFlowEnabled: false + directAccessGrantsEnabled: false + serviceAccountsEnabled: false + publicClient: true + frontchannelLogout: false + protocol: openid-connect + attributes: + pkce.code.challenge.method: S256 + authenticationFlowBindingOverrides: {} + fullScopeAllowed: false + nodeReRegistrationTimeout: 0 + protocolMappers: + - id: 0ddb8d6f-1dc0-4438-9f3f-58b44494ac64 + name: locale + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: locale + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: locale + jsonType.label: String + defaultClientScopes: + - web-origins + - profile + - roles + - email + optionalClientScopes: + - address + - phone + - offline_access + - microprofile-jwt + clientScopes: + - id: ecc31530-edfc-4b32-a590-ff2bb3196a2f + name: microprofile-jwt + description: Microprofile - JWT built-in scope + protocol: openid-connect + attributes: + include.in.token.scope: 'true' + display.on.consent.screen: 'false' + protocolMappers: + - id: ae7b37a8-64ac-4e76-b8ab-506fbbe361db + name: upn + protocol: openid-connect + protocolMapper: oidc-usermodel-property-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: username + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: upn + jsonType.label: String + - id: 73601a4f-3458-4c5c-b477-2643cba7af69 + name: groups + protocol: openid-connect + protocolMapper: oidc-usermodel-realm-role-mapper + consentRequired: false + config: + multivalued: 'true' + user.attribute: foo + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: groups + jsonType.label: String + - id: fa7ec00a-9b33-41f5-aaf9-40e039c81819 + name: offline_access + description: 'OpenID Connect built-in scope: offline_access' + protocol: openid-connect + attributes: + consent.screen.text: "${offlineAccessScopeConsentText}" + display.on.consent.screen: 'true' + - id: aa3ddce8-c8b1-4878-ad5f-8ea1a8751ff5 + name: address + description: 'OpenID Connect built-in scope: address' + protocol: openid-connect + attributes: + include.in.token.scope: 'true' + display.on.consent.screen: 'true' + consent.screen.text: "${addressScopeConsentText}" + protocolMappers: + - id: 82c7b138-ae7c-4106-9e3d-4b8a0febf737 + name: address + protocol: openid-connect + protocolMapper: oidc-address-mapper + consentRequired: false + config: + user.attribute.formatted: formatted + user.attribute.country: country + user.attribute.postal_code: postal_code + userinfo.token.claim: 'true' + user.attribute.street: street + id.token.claim: 'true' + user.attribute.region: region + access.token.claim: 'true' + user.attribute.locality: locality + - id: a4a63ca3-6eba-44ba-acc3-098e3fea5866 + name: profile + description: 'OpenID Connect built-in scope: profile' + protocol: openid-connect + attributes: + include.in.token.scope: 'true' + display.on.consent.screen: 'true' + consent.screen.text: "${profileScopeConsentText}" + protocolMappers: + - id: 3238cfd9-2d1f-4597-8942-063163d61bb6 + name: family name + protocol: openid-connect + protocolMapper: oidc-usermodel-property-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: lastName + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: family_name + jsonType.label: String + - id: 1b3aa687-e407-4d59-a7b6-987e0cfa7d17 + name: username + protocol: openid-connect + protocolMapper: oidc-usermodel-property-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: username + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: preferred_username + jsonType.label: String + - id: 7a6f9b34-4c02-4b27-98c4-6f75dca53a9f + name: updated at + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: updatedAt + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: updated_at + jsonType.label: String + - id: 88303fbe-1894-4db7-8699-334373f288ce + name: full name + protocol: openid-connect + protocolMapper: oidc-full-name-mapper + consentRequired: false + config: + id.token.claim: 'true' + access.token.claim: 'true' + userinfo.token.claim: 'true' + - id: e137e9ac-23cd-4ab9-a00d-7f1eb033d430 + name: given name + protocol: openid-connect + protocolMapper: oidc-usermodel-property-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: firstName + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: given_name + jsonType.label: String + - id: 5085b73e-6a8a-4564-a942-69869170d707 + name: middle name + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: middleName + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: middle_name + jsonType.label: String + - id: a381d7e8-0a34-4afa-ad15-fe3a4129e40d + name: gender + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: gender + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: gender + jsonType.label: String + - id: c617aea6-a25c-4862-8b07-6448b55c863b + name: zoneinfo + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: zoneinfo + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: zoneinfo + jsonType.label: String + - id: 564e11ea-c489-4100-8ae6-8ac18589a6f7 + name: nickname + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: nickname + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: nickname + jsonType.label: String + - id: 31d5a631-44a3-4c0b-8f58-a35c59ff27d2 + name: profile + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: profile + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: profile + jsonType.label: String + - id: 6203f059-62fa-430e-8ad2-3ed5ad9d8a28 + name: website + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: website + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: website + jsonType.label: String + - id: 4c127c38-28b8-4336-89e0-35817f7de486 + name: birthdate + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: birthdate + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: birthdate + jsonType.label: String + - id: 9793c2e9-da3c-4ea7-8921-41ac2f342871 + name: picture + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: picture + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: picture + jsonType.label: String + - id: 8e1a1db5-c0c2-4b80-9482-0bbb0bb6cc44 + name: locale + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: locale + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: locale + jsonType.label: String + - id: 39625d61-d028-46e5-ab31-ece2729ca40d + name: phone + description: 'OpenID Connect built-in scope: phone' + protocol: openid-connect + attributes: + include.in.token.scope: 'true' + display.on.consent.screen: 'true' + consent.screen.text: "${phoneScopeConsentText}" + protocolMappers: + - id: 224df6d4-4fce-471b-8613-1d8b155d7707 + name: phone number verified + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: phoneNumberVerified + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: phone_number_verified + jsonType.label: boolean + - id: 737d9256-29fc-4f28-814e-d4b06caf8675 + name: phone number + protocol: openid-connect + protocolMapper: oidc-usermodel-attribute-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: phoneNumber + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: phone_number + jsonType.label: String + - id: 07d20365-6c6b-4339-bab0-16981d98176c + name: role_list + description: SAML role list + protocol: saml + attributes: + consent.screen.text: "${samlRoleListScopeConsentText}" + display.on.consent.screen: 'true' + protocolMappers: + - id: 5f557a3c-9286-4d4f-a661-67bd7911ca45 + name: role list + protocol: saml + protocolMapper: saml-role-list-mapper + consentRequired: false + config: + single: 'false' + attribute.nameformat: Basic + attribute.name: Role + - id: 89d71aba-11f1-4ca7-92e2-24d648803ebd + name: roles + description: OpenID Connect scope for add user roles to the access token + protocol: openid-connect + attributes: + include.in.token.scope: 'false' + display.on.consent.screen: 'true' + consent.screen.text: "${rolesScopeConsentText}" + protocolMappers: + - id: 4cc3d1e3-46d9-4f9f-9eca-b8553562233c + name: client roles + protocol: openid-connect + protocolMapper: oidc-usermodel-client-role-mapper + consentRequired: false + config: + user.attribute: foo + access.token.claim: 'true' + claim.name: resource_access.${client_id}.roles + jsonType.label: String + multivalued: 'true' + - id: b7fa3a7b-e8b5-4f64-aec7-8f6d19d038c9 + name: realm roles + protocol: openid-connect + protocolMapper: oidc-usermodel-realm-role-mapper + consentRequired: false + config: + user.attribute: foo + access.token.claim: 'true' + claim.name: realm_access.roles + jsonType.label: String + multivalued: 'true' + - id: 77745c36-2d5e-45c9-9a75-aecc4a5ce746 + name: audience resolve + protocol: openid-connect + protocolMapper: oidc-audience-resolve-mapper + consentRequired: false + config: {} + - id: c02a1055-c804-4178-8d7e-29dd5e02960e + name: web-origins + description: OpenID Connect scope for add allowed web origins to the access token + protocol: openid-connect + attributes: + include.in.token.scope: 'false' + display.on.consent.screen: 'false' + consent.screen.text: '' + protocolMappers: + - id: bf82da2c-a436-442d-bb3b-59792a972d5e + name: allowed web origins + protocol: openid-connect + protocolMapper: oidc-allowed-origins-mapper + consentRequired: false + config: {} + - id: c5fc8764-6f26-4116-80bb-58d6d9a2a05d + name: email + description: 'OpenID Connect built-in scope: email' + protocol: openid-connect + attributes: + include.in.token.scope: 'true' + display.on.consent.screen: 'true' + consent.screen.text: "${emailScopeConsentText}" + protocolMappers: + - id: 36c022a6-0f1f-4340-8db2-2fd1ed3a9cc5 + name: email verified + protocol: openid-connect + protocolMapper: oidc-usermodel-property-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: emailVerified + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: email_verified + jsonType.label: boolean + - id: b1c410b3-d19d-4477-a3cb-2d19e1d2155d + name: email + protocol: openid-connect + protocolMapper: oidc-usermodel-property-mapper + consentRequired: false + config: + userinfo.token.claim: 'true' + user.attribute: email + id.token.claim: 'true' + access.token.claim: 'true' + claim.name: email + jsonType.label: String + defaultDefaultClientScopes: + - role_list + - profile + - email + - roles + - web-origins + defaultOptionalClientScopes: + - offline_access + - address + - phone + - microprofile-jwt + browserSecurityHeaders: + contentSecurityPolicyReportOnly: '' + xContentTypeOptions: nosniff + xRobotsTag: none + xFrameOptions: SAMEORIGIN + contentSecurityPolicy: frame-src 'self'; frame-ancestors 'self'; object-src 'none'; + xXSSProtection: 1; mode=block + strictTransportSecurity: max-age=31536000; includeSubDomains + smtpServer: {} + eventsEnabled: false + eventsListeners: + - jboss-logging + enabledEventTypes: [] + adminEventsEnabled: false + adminEventsDetailsEnabled: false + identityProviders: [] + identityProviderMappers: [] + components: + org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy: + - id: d6442b11-c554-47ef-b6e1-69a5a0000364 + name: Consent Required + providerId: consent-required + subType: anonymous + subComponents: {} + config: {} + - id: 406d8415-c40f-4649-b724-30ba83d09a02 + name: Full Scope Disabled + providerId: scope + subType: anonymous + subComponents: {} + config: {} + - id: 20e9c9db-106e-447c-a193-f8c0d8cf9ed7 + name: Trusted Hosts + providerId: trusted-hosts + subType: anonymous + subComponents: {} + config: + host-sending-registration-request-must-match: + - 'true' + client-uris-must-match: + - 'true' + - id: 1a60d807-6ddd-46dc-af19-e674e9f44542 + name: Allowed Protocol Mapper Types + providerId: allowed-protocol-mappers + subType: authenticated + subComponents: {} + config: + allowed-protocol-mapper-types: + - oidc-full-name-mapper + - oidc-address-mapper + - saml-role-list-mapper + - saml-user-property-mapper + - oidc-sha256-pairwise-sub-mapper + - oidc-usermodel-attribute-mapper + - oidc-usermodel-property-mapper + - saml-user-attribute-mapper + - id: 903f4cc5-6c44-4c05-9f9b-984138e60544 + name: Allowed Client Scopes + providerId: allowed-client-templates + subType: authenticated + subComponents: {} + config: + allow-default-scopes: + - 'true' + - id: 29a13944-475a-477a-977c-6ef89725c085 + name: Max Clients Limit + providerId: max-clients + subType: anonymous + subComponents: {} + config: + max-clients: + - '200' + - id: 4041fe42-8b4b-4e85-a109-9236fab6b324 + name: Allowed Protocol Mapper Types + providerId: allowed-protocol-mappers + subType: anonymous + subComponents: {} + config: + allowed-protocol-mapper-types: + - oidc-usermodel-attribute-mapper + - oidc-sha256-pairwise-sub-mapper + - oidc-address-mapper + - saml-user-attribute-mapper + - oidc-usermodel-property-mapper + - saml-role-list-mapper + - saml-user-property-mapper + - oidc-full-name-mapper + - id: 77a52ff4-148e-4b06-9dc6-3516d968b2ce + name: Allowed Client Scopes + providerId: allowed-client-templates + subType: anonymous + subComponents: {} + config: + allow-default-scopes: + - 'true' + org.keycloak.keys.KeyProvider: + - id: 8cace249-1435-4621-8108-93341221b28f + name: rsa-enc-generated + providerId: rsa-enc-generated + subComponents: {} + config: + privateKey: + - MIIEpAIBAAKCAQEAj5zfa0uOR1AdSOdbAWt9CN290kSX61pzklP+cu/P2cDNHPS7h+nzrO4Le678bB8Y6u0akFOqtVzv+CR4Yoy/3VH7QnTV7FYGWqBAFhPEGGUtzdjY4xlxjZaBEItcbrzbzDV6GfVhsdK2ckAxcnXRW91ElyTx5VAYJ4s7yC7LSzLFMSwPcdi5KT6emotfi4RWnGfElRgw+YZapr+3jt77QME8vLSHh0OTtsZZUz5RMR67lC/QacN6M8lxJq1ue5S2UwocviZQBkgofZU2WoEwfjVfm0GJeBOuelalG6rds7D3uS0CxxnSPBjYi/lmQy8PPCstJEp0G8fJLc3C+KTLHQIDAQABAoIBABy3oNGCxUurUH/Qi5koFlOci6WtQ7ezWaLsGth+7dA8RofAxHM0LB7rZu5vmlhAi6oGiaZMpLkpgW7cVBpYzNED4Lt0Q4bD2PdsTgRcJX0/Vj5wW0ZmQxet/dcCFxSpvUYDd4wTTlrRqNwFzB14Q8ob3+hdYeWZ84qMxAKOoOZDTmx8BRvi2JU99aEKCGQVK/pnFkY6ImES3MPPYzb5xOCV79ElY4W+PFrkjmAaI35CjBmSMyu1Fsw+YQWjiqaCnCBUsdtnar4dTUsr+qfMsXP8OAd2RBEGCfcaQstwQJk+/JWKS6XpisGtlrKKfCbBrDDtBkS+AwGoZC3TOFWzyX0CgYEA2ftFwZtiAnBTfh5lb5lX61fxLWmxuvRdzjfpaj2J6odIUEppWRGCloIvZO+hbPR2RGodaLsMi/hOLwcuL+xQwt2WU32n2kDVokTLjTXth2rhhOBTwqop6yCQJI8DuNDGnZxhz5cY6kfrbI9oC8aXls/Gex/nDWOqZipvdvVSneMCgYEAqKkWCOv43PgfIV0xTEiQUUF5IuTVYsFKbLIcbjTt7QcqMQzTyvzSAHIWCoJFo4LiR9lycRmVOUIKp/TIl0PRaFrFiNM7ZSgTihmV9Mju+rxxwvGApXkIlrsmjjC99a5lKz0lMRzgIjOUa4jmeK6ugKzBYgceHf+keKI3IIfXQv8CgYEAinz2m4OBqDT/BqB/J4DP98hehNCixzlbo5moJQRF7AfY7JHqDllukvrQ65rG/zbtMJgOaSx1UzQFUCGKuY203aj0ScUKcEJCuB5nCCcb6q3/63CuJn3/tc9xZJir765MkXP6PG4tuSLKMqWFn/2i74DABBeHrt0ENHZ/bJ99xqsCgYEAgs0vY5fuFyDus6dctjaIhhvq4F7sBny1RXsUhXvTEcI+vG+gSYqtKt9PrLK/Y0H8T5CaKpCWpCNNtFgowPc9jlrnW2fGZnsgPDf/jinO/PHsoC0/ghVNzegyzI+Mot6vY0s2btJgGOY7svInG20NtIlGKMowVz+NxGi5rCbtkO0CgYBneU5jFiH0vdL0Hb5AbRynDM+pDCT+4zwG5XuwcaqWDzMlILiF5tb4P6nPnD8zXbRcLAsu1Be24+6hPgyN8zQVJtM9PNSaIN4zgZ9T4oqCDhF5q68bD1zTLPpnopzdRaeZynhqfhPZZj+xx07gIGiqxx94W5EjM2KuIurMA9FYnQ== + keyUse: + - ENC + certificate: + - MIICmzCCAYMCBgF+Q5OajjANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZjb3VudDAwHhcNMjIwMTEwMTAzOTEzWhcNMzIwMTEwMTA0MDUzWjARMQ8wDQYDVQQDDAZjb3VudDAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCPnN9rS45HUB1I51sBa30I3b3SRJfrWnOSU/5y78/ZwM0c9LuH6fOs7gt7rvxsHxjq7RqQU6q1XO/4JHhijL/dUftCdNXsVgZaoEAWE8QYZS3N2NjjGXGNloEQi1xuvNvMNXoZ9WGx0rZyQDFyddFb3USXJPHlUBgnizvILstLMsUxLA9x2LkpPp6ai1+LhFacZ8SVGDD5hlqmv7eO3vtAwTy8tIeHQ5O2xllTPlExHruUL9Bpw3ozyXEmrW57lLZTChy+JlAGSCh9lTZagTB+NV+bQYl4E656VqUbqt2zsPe5LQLHGdI8GNiL+WZDLw88Ky0kSnQbx8ktzcL4pMsdAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAE1dmmhuTDnlV1vDXjmblX43t0Y3MbNIN1xjNgYGT0rWXEAGkbzcc0737//qgkaPJRgB31QYF4Gf96VCrjteccN+9KqVYqmLOxR1t4kvku9d5ngjvu67TpwdRE55ZCtxcQnsc/bWQ2A8dtHzN+t2N7SyGcouwgWgRLdKN3u0/VR2x/CXWfW2oGOn9haqVwPvq1h8hvNppq13hIbIsgk7WszS3mS2S2krwpzT1rCg1NkTwqBCvaknH8xtVW0UPFVXpCbEpLvJpW5Tg960yEzr2COADq2maaOMY0FAlJfSiFIfZeDCaGg7k6fjA+ie2fz/eaC8Umwn81jJ8VwWAuRYpmc= + priority: + - '100' + algorithm: + - RSA-OAEP + - id: 276936ea-cab7-44f3-a53e-f22b385d4ccf + name: hmac-generated + providerId: hmac-generated + subComponents: {} + config: + kid: + - cf46b046-a67f-4bac-97c2-34734255d684 + secret: + - S5wpZlTvlK-SP7aq9POCWteEoPLHdMYmylYaszygthd8TgbdP1-ChgxgBsczgNUT9ohnt6no04vooV4WQmJvlQ + priority: + - '100' + algorithm: + - HS256 + - id: 6cc34748-da8a-41e3-b595-97b7930ca250 + name: rsa-generated + providerId: rsa-generated + subComponents: {} + config: + privateKey: + - MIIEowIBAAKCAQEAs7rVeqkys7yWA3ONKhmamLHOcrem9reOet2dn2G8HFFPDPrRZSvDpWll70AjwYUQEdpEjrKk20vH2ZbCsq++Dt1ewMzmuiZclZBxpDrn7lOjIxuG2pocOtzahAammYBtAtkW6MuGmOgw1WwKNOMfnSukgx+4t1lLSzs55jnikNRycXrm6y5ad7v/vX4fGUs+GRSTO7UCRR6nhlz4DR9RSpQfCONsE8K0a/DAvMGNVKovIPi/7D5reLXxpylr/2ojLBClO0LQqwrC/WAUb7AiJgbKWTlcebZSB9Ei9mh/5B1U7xTPoHdcQInt7vPKKx7TMMmhm1j3m9PMRHzLD2lvlwIDAQABAoIBAAIlApMaHb7DS07zPAX6lDuqM3pu8pETE6Inrs/ODL6Rwc232HPKl+ULYun96+9NNSnhXtwNCaaMOvA/ukcDjdMDlTPbvg0OlCA8vv5krYvMd6/djjhhimCxbfIRWj+Opr5X9MwGUa7VZm/FgEGtTB1F/gqKgFu/twRIyqISor9zpFLnz4luyom65x9AfeoBE4C7vkPynZa2/lH4n6ihhc+1BkDjTyvL5RgI6Z5sNob9hvF0+urBVrm3Y2AxyfMfdgfA2qR/iICKJyAZi8OPls9X6nOmJhTauIeEdNG9GQT2u3HxCgiHL21WYq4hVuVV3JHjGINGw9zaxMT/rd1kifECgYEA+28t/RI0KKy4gpe+G6m042DVzVtFzrXfnAZm6/e7UwTLWzjrVKyvDD1M84vWQbyHdUrywB3kvwcxz7euk0Bb4Uwjr0rUmOwzZBZLNes931EQx6vE2oeYoqmKrRIkd9Gs1E0bltKx5C78F0vtwpHmz5tIwwF3oP/SVG8w57yLcBsCgYEAtv5T+H/Tky1oCd0zhOkLDtVM2Z6sPkEkhE8zto7IrVaInTZAF7IFnbrAEYAyWZq8nA0LxPeDvxXRCImdgA8gNljC7aPE+DZxV54vwgBnAlzoVAG4CH33QfM7OEh3gdT53Lqx3Uh2Qt08pVz1+vAM1S4qUGcMLXxfN77jgMNZTjUCgYEA6iaXxV32hQqUqcl2mXxpoHbFpQCi+eYV3892ebmzEZUdbE6NmcVXHybXStenKIDSBUFO3+r2449nq/F6+latOhsWAGDHq8IL+eFpGUWB0T5FSi2EnZ45XwJUyuhiXdM+CFfmoYaFc+LtkSR8vv3w3NXX5QKwzZZv4YHLIYRMtpECgYA1LBN0OphcxK3dZ+QHc7vd1IbfGScNc9pLg8QQAM845tMNc20ONZFCMriKnUiEFt1FLtlDo3QpuwohQ/N6+WovwHzrllGumgs3HWTdJ0bHPf3YIyO5e/izthx4Dz6CgEMWKz1xghOy/BwaJLfo8YWZEDAFatvz/5afWR08FgdGHQKBgBaAMRn2t++Jdxm0Wk79HRmaVSrOwP6WNpToQWm/PpQouoaEnyfNarf3IPDSNrFYgoeWJc34c00GyFBs7Uljjmk3jYH5EqOdVPiSS/YmhAGS8vo4uyHTDrtIkjWkkZPuRSZd3jeUyn6tgJf2YKY0jciDrnRlsaPy9prZEpLmtIix + keyUse: + - SIG + certificate: + - MIICmzCCAYMCBgF+Q5OZ1TANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZjb3VudDAwHhcNMjIwMTEwMTAzOTEzWhcNMzIwMTEwMTA0MDUzWjARMQ8wDQYDVQQDDAZjb3VudDAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCzutV6qTKzvJYDc40qGZqYsc5yt6b2t4563Z2fYbwcUU8M+tFlK8OlaWXvQCPBhRAR2kSOsqTbS8fZlsKyr74O3V7AzOa6JlyVkHGkOufuU6MjG4bamhw63NqEBqaZgG0C2Rboy4aY6DDVbAo04x+dK6SDH7i3WUtLOznmOeKQ1HJxeubrLlp3u/+9fh8ZSz4ZFJM7tQJFHqeGXPgNH1FKlB8I42wTwrRr8MC8wY1Uqi8g+L/sPmt4tfGnKWv/aiMsEKU7QtCrCsL9YBRvsCImBspZOVx5tlIH0SL2aH/kHVTvFM+gd1xAie3u88orHtMwyaGbWPeb08xEfMsPaW+XAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKwbmH0289mlqrs6wK6a2uT7PhT+vnB4SL0i1xXKgeWZtd5Uikynxuu0yvV7PKVcG4VtaK1Gz9kcFw9tU+gjyuiebSI4MkiKCGDtot7Jf5MqFsAZOEFjO8dWoYm/XT9kFyP8xGBafWuy3UvvWUvBIkhGmhtIJsOjQ8ab8KsUvRX2xQVEYJVkvHbNw4bZWsRJukiyILLaSV+pVgRf35McczvFD6ZmgJyXlzs3BuO1TxkzGceuWuO2oT0/ygGNBi5D3yBrSbL2sXhTCozf++fqvD8nYLoHxxmjtj8BreDLz4UceeuVQ3eb6pH19AheEL+44oWkoroCh3K+PnRSPvkrsII= + priority: + - '100' + - id: e435e7cb-6d41-47f7-b019-cea2d65cd776 + name: aes-generated + providerId: aes-generated + subComponents: {} + config: + kid: + - 80aec488-3bdc-454f-8113-d7b3d1211bb8 + secret: + - 8VZ6d3C4um6pyB4jPc9jhw + priority: + - '100' + internationalizationEnabled: false + supportedLocales: [] + authenticationFlows: + - id: faed7652-9765-494a-ba3a-ce7a9d69d0eb + alias: Account verification options + description: Method with which to verity the existing account + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: idp-email-verification + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: ALTERNATIVE + priority: 20 + flowAlias: Verify Existing Account by Re-authentication + userSetupAllowed: false + autheticatorFlow: true + - id: c4bc9194-9ab0-46a3-966f-686c6f39026e + alias: Authentication Options + description: Authentication options. + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: basic-auth + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: basic-auth-otp + authenticatorFlow: false + requirement: DISABLED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: auth-spnego + authenticatorFlow: false + requirement: DISABLED + priority: 30 + userSetupAllowed: false + autheticatorFlow: false + - id: 7d4ed634-e61f-4245-b117-8e64f19f0cbd + alias: Browser - Conditional OTP + description: Flow to determine if the OTP is required for the authentication + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: conditional-user-configured + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: auth-otp-form + authenticatorFlow: false + requirement: REQUIRED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - id: 79c88077-d077-4b2b-b318-018c71b22f94 + alias: Direct Grant - Conditional OTP + description: Flow to determine if the OTP is required for the authentication + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: conditional-user-configured + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: direct-grant-validate-otp + authenticatorFlow: false + requirement: REQUIRED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - id: 0711a798-7630-47f2-93a9-4a241883fd10 + alias: First broker login - Conditional OTP + description: Flow to determine if the OTP is required for the authentication + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: conditional-user-configured + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: auth-otp-form + authenticatorFlow: false + requirement: REQUIRED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - id: 0b526122-b897-4201-8eef-bec54e545d09 + alias: Handle Existing Account + description: Handle what to do if there is existing account with same email/username + like authenticated identity provider + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: idp-confirm-link + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: REQUIRED + priority: 20 + flowAlias: Account verification options + userSetupAllowed: false + autheticatorFlow: true + - id: 3453f13a-f65f-4548-acd4-41b113deff4c + alias: Reset - Conditional OTP + description: Flow to determine if the OTP should be reset or not. Set to REQUIRED + to force. + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: conditional-user-configured + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: reset-otp + authenticatorFlow: false + requirement: REQUIRED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - id: 376a76cb-b1ec-476f-8765-1038565e7b07 + alias: User creation or linking + description: Flow for the existing/non-existing user alternatives + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticatorConfig: create unique user config + authenticator: idp-create-user-if-unique + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: ALTERNATIVE + priority: 20 + flowAlias: Handle Existing Account + userSetupAllowed: false + autheticatorFlow: true + - id: 4824971c-53d8-40a4-ad70-2f9c52c58efb + alias: Verify Existing Account by Re-authentication + description: Reauthentication of existing account + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: idp-username-password-form + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: CONDITIONAL + priority: 20 + flowAlias: First broker login - Conditional OTP + userSetupAllowed: false + autheticatorFlow: true + - id: 6fdbec3d-a275-4f3c-ac07-e39186b3c095 + alias: browser + description: browser based authentication + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: auth-cookie + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: auth-spnego + authenticatorFlow: false + requirement: DISABLED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: identity-provider-redirector + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 25 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: ALTERNATIVE + priority: 30 + flowAlias: forms + userSetupAllowed: false + autheticatorFlow: true + - id: 051a345a-fe24-42e3-9850-17537cdf846d + alias: clients + description: Base authentication for clients + providerId: client-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: client-secret + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: client-jwt + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: client-secret-jwt + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 30 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: client-x509 + authenticatorFlow: false + requirement: ALTERNATIVE + priority: 40 + userSetupAllowed: false + autheticatorFlow: false + - id: 4bcfaa9e-e23e-4a49-ae37-d9e635339816 + alias: direct grant + description: OpenID Connect Resource Owner Grant + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: direct-grant-validate-username + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: direct-grant-validate-password + authenticatorFlow: false + requirement: REQUIRED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: CONDITIONAL + priority: 30 + flowAlias: Direct Grant - Conditional OTP + userSetupAllowed: false + autheticatorFlow: true + - id: 78f4d173-44c2-4dbe-b1b6-2b86f90d836e + alias: docker auth + description: Used by Docker clients to authenticate against the IDP + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: docker-http-basic-authenticator + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - id: 98a30528-5f73-4eb3-b89b-7bf06cbbc47d + alias: first broker login + description: Actions taken after first broker login with identity provider account, + which is not yet linked to any Keycloak account + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticatorConfig: review profile config + authenticator: idp-review-profile + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: REQUIRED + priority: 20 + flowAlias: User creation or linking + userSetupAllowed: false + autheticatorFlow: true + - id: a25ad287-43c1-4dcd-aca5-f7b5e5907780 + alias: forms + description: Username, password, otp and other auth forms. + providerId: basic-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: auth-username-password-form + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: CONDITIONAL + priority: 20 + flowAlias: Browser - Conditional OTP + userSetupAllowed: false + autheticatorFlow: true + - id: c23d0e26-4b72-4834-b184-67bb6120115b + alias: http challenge + description: An authentication flow based on challenge-response HTTP Authentication + Schemes + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: no-cookie-redirect + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: REQUIRED + priority: 20 + flowAlias: Authentication Options + userSetupAllowed: false + autheticatorFlow: true + - id: fabd90c2-92a2-41a2-bf04-5edf88890f9a + alias: registration + description: registration flow + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: registration-page-form + authenticatorFlow: true + requirement: REQUIRED + priority: 10 + flowAlias: registration form + userSetupAllowed: false + autheticatorFlow: true + - id: 7e271f7e-0275-49b5-9f92-4bd6b4d4ae69 + alias: registration form + description: registration form + providerId: form-flow + topLevel: false + builtIn: true + authenticationExecutions: + - authenticator: registration-user-creation + authenticatorFlow: false + requirement: REQUIRED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: registration-profile-action + authenticatorFlow: false + requirement: REQUIRED + priority: 40 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: registration-password-action + authenticatorFlow: false + requirement: REQUIRED + priority: 50 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: registration-recaptcha-action + authenticatorFlow: false + requirement: DISABLED + priority: 60 + userSetupAllowed: false + autheticatorFlow: false + - id: ad20fc9c-ea61-4fd0-8bda-ada4f4f159e5 + alias: reset credentials + description: Reset credentials for a user if they forgot their password or something + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: reset-credentials-choose-user + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: reset-credential-email + authenticatorFlow: false + requirement: REQUIRED + priority: 20 + userSetupAllowed: false + autheticatorFlow: false + - authenticator: reset-password + authenticatorFlow: false + requirement: REQUIRED + priority: 30 + userSetupAllowed: false + autheticatorFlow: false + - authenticatorFlow: true + requirement: CONDITIONAL + priority: 40 + flowAlias: Reset - Conditional OTP + userSetupAllowed: false + autheticatorFlow: true + - id: 1081e874-c7b0-42db-861f-1e4ca34af878 + alias: saml ecp + description: SAML ECP Profile Authentication Flow + providerId: basic-flow + topLevel: true + builtIn: true + authenticationExecutions: + - authenticator: http-basic-authenticator + authenticatorFlow: false + requirement: REQUIRED + priority: 10 + userSetupAllowed: false + autheticatorFlow: false + authenticatorConfig: + - id: '009d3d66-0a89-4c03-8b15-f031c0afc28c' + alias: create unique user config + config: + require.password.update.after.registration: 'false' + - id: a25071db-f600-4e5b-9c0d-dee20f15d1bf + alias: review profile config + config: + update.profile.on.first.login: missing + requiredActions: + - alias: CONFIGURE_TOTP + name: Configure OTP + providerId: CONFIGURE_TOTP + enabled: true + defaultAction: false + priority: 10 + config: {} + - alias: terms_and_conditions + name: Terms and Conditions + providerId: terms_and_conditions + enabled: false + defaultAction: false + priority: 20 + config: {} + - alias: UPDATE_PASSWORD + name: Update Password + providerId: UPDATE_PASSWORD + enabled: true + defaultAction: false + priority: 30 + config: {} + - alias: UPDATE_PROFILE + name: Update Profile + providerId: UPDATE_PROFILE + enabled: true + defaultAction: false + priority: 40 + config: {} + - alias: VERIFY_EMAIL + name: Verify Email + providerId: VERIFY_EMAIL + enabled: true + defaultAction: false + priority: 50 + config: {} + - alias: delete_account + name: Delete Account + providerId: delete_account + enabled: false + defaultAction: false + priority: 60 + config: {} + - alias: update_user_locale + name: Update User Locale + providerId: update_user_locale + enabled: true + defaultAction: false + priority: 1000 + config: {} + browserFlow: browser + registrationFlow: registration + directGrantFlow: direct grant + resetCredentialsFlow: reset credentials + clientAuthenticationFlow: clients + dockerAuthenticationFlow: docker auth + attributes: + cibaBackchannelTokenDeliveryMode: poll + cibaExpiresIn: '120' + cibaAuthRequestedUserHint: login_hint + oauth2DeviceCodeLifespan: '600' + oauth2DevicePollingInterval: '5' + parRequestUriLifespan: '60' + cibaInterval: '5' + keycloakVersion: 16.1.0 + userManagedAccessAllowed: false + clientProfiles: + profiles: [] + clientPolicies: + policies: [] diff --git a/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java b/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java index 692419288e..6b5e33992a 100644 --- a/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java +++ b/operator/src/test/java/org/keycloak/operator/ClusterOperatorTest.java @@ -55,7 +55,7 @@ public abstract class ClusterOperatorTest { createNamespace(); if (operatorDeployment == OperatorDeployment.remote) { - createCRD(); + createCRDs(); createRBACresourcesAndOperatorDeployment(); } else { createOperator(); @@ -90,9 +90,10 @@ public abstract class ClusterOperatorTest { k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER +deploymentTarget+".yml")) .inNamespace(namespace).delete(); } - private static void createCRD() throws FileNotFoundException { - Log.info("Creating CRD "); + private static void createCRDs() throws FileNotFoundException { + Log.info("Creating CRDs"); k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER + "keycloaks.keycloak.org-v1.yml")).createOrReplace(); + k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER + "keycloakrealmimports.keycloak.org-v1.yml")).createOrReplace(); } private static void registerReconcilers() { diff --git a/operator/src/test/java/org/keycloak/operator/RealmImportE2EIT.java b/operator/src/test/java/org/keycloak/operator/RealmImportE2EIT.java new file mode 100644 index 0000000000..2f8466151d --- /dev/null +++ b/operator/src/test/java/org/keycloak/operator/RealmImportE2EIT.java @@ -0,0 +1,169 @@ +package org.keycloak.operator; + +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.extended.run.RunConfigBuilder; +import io.quarkus.logging.Log; +import io.quarkus.test.junit.QuarkusTest; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImport; +import org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition; + +import java.util.List; +import java.util.Map; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition.DONE; +import static org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition.STARTED; +import static org.keycloak.operator.v2alpha1.crds.KeycloakRealmImportStatusCondition.HAS_ERRORS; + +@QuarkusTest +public class RealmImportE2EIT extends ClusterOperatorTest { + + final static String KEYCLOAK_SERVICE_NAME = "example-keycloak"; + final static int KEYCLOAK_PORT = 8080; + + private KeycloakRealmImportStatusCondition getCondition(List conditions, String type) { + return conditions + .stream() + .filter(c -> c.getType().equals(type)) + .findFirst() + .get(); + } + + @Test + public void testWorkingRealmImport() { + Log.info(((operatorDeployment == OperatorDeployment.remote) ? "Remote " : "Local ") + "Run Test :" + namespace); + // Arrange + k8sclient.load(getClass().getResourceAsStream("/example-postgres.yaml")).inNamespace(namespace).createOrReplace(); + k8sclient.load(getClass().getResourceAsStream("/example-keycloak.yml")).inNamespace(namespace).createOrReplace(); + + k8sclient.services().inNamespace(namespace).create( + new ServiceBuilder() + .withNewMetadata() + .withName(KEYCLOAK_SERVICE_NAME) + .withNamespace(namespace) + .endMetadata() + .withNewSpec() + .withSelector(Map.of("app", "keycloak")) + .addNewPort() + .withPort(KEYCLOAK_PORT) + .endPort() + .endSpec() + .build() + ); + + // Act + k8sclient.load(getClass().getResourceAsStream("/example-realm.yaml")).inNamespace(namespace).createOrReplace(); + + // Assert + var crSelector = k8sclient + .resources(KeycloakRealmImport.class) + .inNamespace(namespace) + .withName("example-count0-kc"); + Awaitility.await() + .atMost(3, MINUTES) + .pollDelay(5, SECONDS) + .ignoreExceptions() + .untilAsserted(() -> { + var conditions = crSelector + .get() + .getStatus() + .getConditions(); + + assertThat(getCondition(conditions, DONE).getStatus()).isFalse(); + assertThat(getCondition(conditions, STARTED).getStatus()).isTrue(); + assertThat(getCondition(conditions, HAS_ERRORS).getStatus()).isFalse(); + }); + + Awaitility.await() + .atMost(3, MINUTES) + .pollDelay(5, SECONDS) + .ignoreExceptions() + .untilAsserted(() -> { + var conditions = crSelector + .get() + .getStatus() + .getConditions(); + + assertThat(getCondition(conditions, DONE).getStatus()).isTrue(); + assertThat(getCondition(conditions, STARTED).getStatus()).isFalse(); + assertThat(getCondition(conditions, HAS_ERRORS).getStatus()).isFalse(); + }); + + String url = + "http://" + KEYCLOAK_SERVICE_NAME + "." + namespace + ":" + KEYCLOAK_PORT + "/realms/count0"; + + Awaitility.await().atMost(5, MINUTES).untilAsserted(() -> { + try { + Log.info("Starting curl Pod to test if the realm is available"); + + Pod curlPod = k8sclient.run().inNamespace(namespace) + .withRunConfig(new RunConfigBuilder() + .withArgs("-s", "-o", "/dev/null", "-w", "%{http_code}", url) + .withName("curl") + .withImage("curlimages/curl:7.78.0") + .withRestartPolicy("Never") + .build()) + .done(); + Log.info("Waiting for curl Pod to finish running"); + Awaitility.await().atMost(2, MINUTES) + .until(() -> { + String phase = + k8sclient.pods().inNamespace(namespace).withName("curl").get() + .getStatus().getPhase(); + return phase.equals("Succeeded") || phase.equals("Failed"); + }); + + String curlOutput = + k8sclient.pods().inNamespace(namespace) + .withName(curlPod.getMetadata().getName()).getLog(); + Log.info("Output from curl: '" + curlOutput + "'"); + assertThat(curlOutput).isEqualTo("200"); + } catch (KubernetesClientException ex) { + throw new AssertionError(ex); + } finally { + Log.info("Deleting curl Pod"); + k8sclient.pods().inNamespace(namespace).withName("curl").delete(); + Awaitility.await().atMost(1, MINUTES) + .until(() -> k8sclient.pods().inNamespace(namespace).withName("curl") + .get() == null); + } + }); + } + + @Test + public void testNotWorkingRealmImport() { + Log.info(((operatorDeployment == OperatorDeployment.remote) ? "Remote " : "Local ") + "Run Test :" + namespace); + // Arrange + k8sclient.load(getClass().getResourceAsStream("/example-postgres.yaml")).inNamespace(namespace).createOrReplace(); + k8sclient.load(getClass().getResourceAsStream("/example-keycloak.yml")).inNamespace(namespace).createOrReplace(); + + // Act + k8sclient.load(getClass().getResourceAsStream("/incorrect-realm.yaml")).inNamespace(namespace).createOrReplace(); + + // Assert + Awaitility.await() + .atMost(3, MINUTES) + .pollDelay(5, SECONDS) + .ignoreExceptions() + .untilAsserted(() -> { + var conditions = k8sclient + .resources(KeycloakRealmImport.class) + .inNamespace(namespace) + .withName("example-count0-kc") + .get() + .getStatus() + .getConditions(); + + assertThat(getCondition(conditions, HAS_ERRORS).getStatus()).isTrue(); + assertThat(getCondition(conditions, DONE).getStatus()).isFalse(); + assertThat(getCondition(conditions, STARTED).getStatus()).isFalse(); + }); + } + +} diff --git a/operator/src/test/resources/incorrect-realm.yaml b/operator/src/test/resources/incorrect-realm.yaml new file mode 100644 index 0000000000..933a5d2cd6 --- /dev/null +++ b/operator/src/test/resources/incorrect-realm.yaml @@ -0,0 +1,9 @@ +apiVersion: keycloak.org/v2alpha1 +kind: KeycloakRealmImport +metadata: + name: example-count0-kc +spec: + keycloakCRName: example-kc2 + realm: + id: count0 + realm: count0