Keycloak operator Realm bulk import
This commit is contained in:
parent
d8097ee7a5
commit
98d4436313
18 changed files with 2676 additions and 26 deletions
|
@ -40,13 +40,6 @@
|
||||||
<maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
|
<maven-failsafe-plugin.version>2.22.0</maven-failsafe-plugin.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
|
||||||
<repository>
|
|
||||||
<id>s01.oss.sonatype</id>
|
|
||||||
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
|
|
||||||
</repository>
|
|
||||||
</repositories>
|
|
||||||
|
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import io.fabric8.kubernetes.api.model.OwnerReference;
|
||||||
import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder;
|
import io.fabric8.kubernetes.api.model.OwnerReferenceBuilder;
|
||||||
import io.fabric8.kubernetes.client.CustomResource;
|
import io.fabric8.kubernetes.client.CustomResource;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||||
|
import io.fabric8.kubernetes.client.utils.Serialization;
|
||||||
import io.quarkus.logging.Log;
|
import io.quarkus.logging.Log;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -43,16 +44,23 @@ public abstract class OperatorManagedResource {
|
||||||
this.cr = cr;
|
this.cr = cr;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract HasMetadata getReconciledResource();
|
protected abstract Optional<HasMetadata> getReconciledResource();
|
||||||
|
|
||||||
public void createOrUpdateReconciled() {
|
public void createOrUpdateReconciled() {
|
||||||
HasMetadata resource = getReconciledResource();
|
getReconciledResource().ifPresent(resource -> {
|
||||||
setDefaultLabels(resource);
|
try {
|
||||||
setOwnerReferences(resource);
|
setDefaultLabels(resource);
|
||||||
|
setOwnerReferences(resource);
|
||||||
|
|
||||||
Log.debugf("Creating or updating resource: %s", resource);
|
Log.debugf("Creating or updating resource: %s", resource);
|
||||||
resource = client.resource(resource).createOrReplace();
|
resource = client.resource(resource).createOrReplace();
|
||||||
Log.debugf("Successfully created or updated resource: %s", resource);
|
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) {
|
protected void setDefaultLabels(HasMetadata resource) {
|
||||||
|
|
|
@ -61,7 +61,7 @@ public class KeycloakDeployment extends OperatorManagedResource {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected HasMetadata getReconciledResource() {
|
protected Optional<HasMetadata> getReconciledResource() {
|
||||||
Deployment baseDeployment = new DeploymentBuilder(this.baseDeployment).build(); // clone not to change the base template
|
Deployment baseDeployment = new DeploymentBuilder(this.baseDeployment).build(); // clone not to change the base template
|
||||||
Deployment reconciledDeployment;
|
Deployment reconciledDeployment;
|
||||||
if (existingDeployment == null) {
|
if (existingDeployment == null) {
|
||||||
|
@ -75,7 +75,7 @@ public class KeycloakDeployment extends OperatorManagedResource {
|
||||||
reconciledDeployment.setSpec(baseDeployment.getSpec());
|
reconciledDeployment.setSpec(baseDeployment.getSpec());
|
||||||
}
|
}
|
||||||
|
|
||||||
return reconciledDeployment;
|
return Optional.of(reconciledDeployment);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Deployment fetchExistingDeployment() {
|
private Deployment fetchExistingDeployment() {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,13 +19,19 @@ package org.keycloak.operator.v2alpha1.crds;
|
||||||
import io.fabric8.kubernetes.api.model.Namespaced;
|
import io.fabric8.kubernetes.api.model.Namespaced;
|
||||||
import io.fabric8.kubernetes.client.CustomResource;
|
import io.fabric8.kubernetes.client.CustomResource;
|
||||||
import io.fabric8.kubernetes.model.annotation.Group;
|
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.fabric8.kubernetes.model.annotation.Version;
|
||||||
|
import io.sundr.builder.annotations.Buildable;
|
||||||
|
import io.sundr.builder.annotations.BuildableReference;
|
||||||
import org.keycloak.operator.Constants;
|
import org.keycloak.operator.Constants;
|
||||||
|
|
||||||
@Group(Constants.CRDS_GROUP)
|
@Group(Constants.CRDS_GROUP)
|
||||||
@Version(Constants.CRDS_VERSION)
|
@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 {
|
||||||
|
|
||||||
}
|
}
|
|
@ -16,15 +16,18 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.operator.v2alpha1.crds;
|
package org.keycloak.operator.v2alpha1.crds;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
public class RealmSpec {
|
public class KeycloakRealmImportSpec {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@JsonPropertyDescription("The name of the Keycloak CR to reference, in the same namespace.")
|
||||||
private String keycloakCRName;
|
private String keycloakCRName;
|
||||||
@NotNull
|
@NotNull
|
||||||
|
@JsonPropertyDescription("The RealmRepresentation to import into Keycloak.")
|
||||||
private RealmRepresentation realm;
|
private RealmRepresentation realm;
|
||||||
|
|
||||||
public String getKeycloakCRName() {
|
public String getKeycloakCRName() {
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,6 +15,30 @@ rules:
|
||||||
- delete
|
- delete
|
||||||
- patch
|
- patch
|
||||||
- update
|
- 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
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
kind: RoleBinding
|
kind: RoleBinding
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
quarkus.operator-sdk.crd.apply=true
|
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.container-image.builder=jib
|
||||||
quarkus.operator-sdk.crd.validate=false
|
quarkus.operator-sdk.crd.validate=false
|
||||||
|
|
||||||
|
|
|
@ -32,14 +32,14 @@ spec:
|
||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 15
|
initialDelaySeconds: 15
|
||||||
periodSeconds: 2
|
periodSeconds: 2
|
||||||
failureThreshold: 10
|
failureThreshold: 100
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /health/ready
|
path: /health/ready
|
||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 15
|
initialDelaySeconds: 15
|
||||||
periodSeconds: 2
|
periodSeconds: 2
|
||||||
failureThreshold: 10
|
failureThreshold: 200
|
||||||
dnsPolicy: ClusterFirst
|
dnsPolicy: ClusterFirst
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
terminationGracePeriodSeconds: 30
|
terminationGracePeriodSeconds: 30
|
||||||
|
|
1848
operator/src/main/resources/example-realm.yaml
Normal file
1848
operator/src/main/resources/example-realm.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -55,7 +55,7 @@ public abstract class ClusterOperatorTest {
|
||||||
createNamespace();
|
createNamespace();
|
||||||
|
|
||||||
if (operatorDeployment == OperatorDeployment.remote) {
|
if (operatorDeployment == OperatorDeployment.remote) {
|
||||||
createCRD();
|
createCRDs();
|
||||||
createRBACresourcesAndOperatorDeployment();
|
createRBACresourcesAndOperatorDeployment();
|
||||||
} else {
|
} else {
|
||||||
createOperator();
|
createOperator();
|
||||||
|
@ -90,9 +90,10 @@ public abstract class ClusterOperatorTest {
|
||||||
k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER +deploymentTarget+".yml"))
|
k8sclient.load(new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER +deploymentTarget+".yml"))
|
||||||
.inNamespace(namespace).delete();
|
.inNamespace(namespace).delete();
|
||||||
}
|
}
|
||||||
private static void createCRD() throws FileNotFoundException {
|
private static void createCRDs() throws FileNotFoundException {
|
||||||
Log.info("Creating CRD ");
|
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 + "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() {
|
private static void registerReconcilers() {
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
9
operator/src/test/resources/incorrect-realm.yaml
Normal file
9
operator/src/test/resources/incorrect-realm.yaml
Normal 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
|
Loading…
Reference in a new issue