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>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>s01.oss.sonatype</id>
|
||||
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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.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 {
|
||||
|
||||
}
|
|
@ -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() {
|
|
@ -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
|
||||
- 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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
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();
|
||||
|
||||
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() {
|
||||
|
|
|
@ -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