Keycloak operator Realm bulk import

This commit is contained in:
andreaTP 2022-01-27 17:31:46 +00:00 committed by Pedro Igor
parent d8097ee7a5
commit 98d4436313
18 changed files with 2676 additions and 26 deletions

View file

@ -40,13 +40,6 @@
<maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
</properties>
<repositories>
<repository>
<id>s01.oss.sonatype</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
</repository>
</repositories>
<dependencyManagement>
<dependencies>
<dependency>

View file

@ -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<HasMetadata> getReconciledResource();
public void createOrUpdateReconciled() {
HasMetadata resource = getReconciledResource();
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);
} catch (Exception e) {
Log.error("Failed to create or update resource");
Log.error(Serialization.asYaml(resource));
throw e;
}
});
}
protected void setDefaultLabels(HasMetadata resource) {

View file

@ -61,7 +61,7 @@ public class KeycloakDeployment extends OperatorManagedResource {
}
@Override
protected HasMetadata getReconciledResource() {
protected Optional<HasMetadata> 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() {

View file

@ -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<KeycloakRealmImport>, EventSourceInitializer<KeycloakRealmImport>, ErrorStatusHandler<KeycloakRealmImport> {
@Inject
KubernetesClient client;
@Inject
ObjectMapper jsonMapper;
@Override
public List<EventSource> prepareEventSources(EventSourceContext<KeycloakRealmImport> context) {
SharedIndexInformer<Job> 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<KeycloakRealmImport> 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<KeycloakRealmImport> 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);
}
}

View file

@ -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<HasMetadata> 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();
}
}

View file

@ -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<HasMetadata> 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;
}
}

View file

@ -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<RealmSpec, Void> 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<KeycloakRealmImportSpec, KeycloakRealmImportStatus> implements Namespaced {
}

View file

@ -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() {

View file

@ -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<KeycloakRealmImportStatusCondition> conditions;
public List<KeycloakRealmImportStatusCondition> getConditions() {
return conditions;
}
public void setConditions(List<KeycloakRealmImportStatusCondition> 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());
}
}

View file

@ -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<String> notReadyMessages = new ArrayList<>();
private final List<String> startedMessages = new ArrayList<>();
private final List<String> 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;
}
}

View file

@ -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());
}
}

View file

@ -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

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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() {

View file

@ -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<KeycloakRealmImportStatusCondition> 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();
});
}
}

View file

@ -0,0 +1,9 @@
apiVersion: keycloak.org/v2alpha1
kind: KeycloakRealmImport
metadata:
name: example-count0-kc
spec:
keycloakCRName: example-kc2
realm:
id: count0
realm: count0