Baseline for Keycloak deployment in operator
This commit is contained in:
parent
d28b54e5d5
commit
6b485b8603
13 changed files with 573 additions and 134 deletions
32
operator/src/main/java/org/keycloak/operator/Config.java
Normal file
32
operator/src/main/java/org/keycloak/operator/Config.java
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import io.smallrye.config.ConfigMapping;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
@ConfigMapping(prefix = "operator")
|
||||
public interface Config {
|
||||
Keycloak keycloak();
|
||||
|
||||
interface Keycloak {
|
||||
String image();
|
||||
}
|
||||
}
|
|
@ -28,9 +28,11 @@ public final class Constants {
|
|||
public static final String MANAGED_BY_VALUE = "keycloak-operator";
|
||||
|
||||
public static final Map<String, String> DEFAULT_LABELS = Map.of(
|
||||
"app", NAME
|
||||
"app", NAME,
|
||||
MANAGED_BY_LABEL, MANAGED_BY_VALUE
|
||||
);
|
||||
|
||||
public static final String DEFAULT_KEYCLOAK_IMAGE = "quay.io/keycloak/keycloak-x:latest";
|
||||
public static final String DEFAULT_KEYCLOAK_INIT_IMAGE = "quay.io/keycloak/keycloak-init-container:latest";
|
||||
public static final Map<String, String> DEFAULT_DIST_CONFIG = Map.of(
|
||||
"KEYCLOAK_METRICS_ENABLED", "true"
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||
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.quarkus.logging.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a single K8s resource that is managed by this operator (e.g. Deployment, Service, Ingress, etc.)
|
||||
*
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public abstract class OperatorManagedResource {
|
||||
protected KubernetesClient client;
|
||||
protected CustomResource<?, ?> cr;
|
||||
|
||||
public OperatorManagedResource(KubernetesClient client, CustomResource<?, ?> cr) {
|
||||
this.client = client;
|
||||
this.cr = cr;
|
||||
}
|
||||
|
||||
protected abstract HasMetadata getReconciledResource();
|
||||
|
||||
public void createOrUpdateReconciled() {
|
||||
HasMetadata resource = getReconciledResource();
|
||||
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);
|
||||
}
|
||||
|
||||
protected void setDefaultLabels(HasMetadata resource) {
|
||||
Map<String, String> labels = Optional.ofNullable(resource.getMetadata().getLabels()).orElse(new HashMap<>());
|
||||
labels.putAll(Constants.DEFAULT_LABELS);
|
||||
resource.getMetadata().setLabels(labels);
|
||||
}
|
||||
|
||||
protected void setOwnerReferences(HasMetadata resource) {
|
||||
if (!cr.getMetadata().getNamespace().equals(resource.getMetadata().getNamespace())) {
|
||||
return;
|
||||
}
|
||||
|
||||
OwnerReference owner = new OwnerReferenceBuilder()
|
||||
.withApiVersion(cr.getApiVersion())
|
||||
.withKind(cr.getKind())
|
||||
.withName(cr.getMetadata().getName())
|
||||
.withUid(cr.getMetadata().getUid())
|
||||
.withBlockOwnerDeletion(true)
|
||||
.withController(true)
|
||||
.build();
|
||||
|
||||
resource.getMetadata().setOwnerReferences(Collections.singletonList(owner));
|
||||
}
|
||||
}
|
|
@ -18,59 +18,101 @@ package org.keycloak.operator.v2alpha1;
|
|||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.OwnerReference;
|
||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.*;
|
||||
import io.javaoperatorsdk.operator.api.reconciler.Constants;
|
||||
import org.jboss.logging.Logger;
|
||||
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.ResourceID;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
|
||||
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
|
||||
import io.quarkus.logging.Log;
|
||||
import org.keycloak.operator.Config;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.v2alpha1.crds.Keycloak;
|
||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatus;
|
||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder;
|
||||
|
||||
@ControllerConfiguration(namespaces = Constants.WATCH_CURRENT_NAMESPACE, finalizerName = Constants.NO_FINALIZER)
|
||||
public class KeycloakController implements Reconciler<Keycloak> {
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Inject
|
||||
Logger logger;
|
||||
import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE;
|
||||
|
||||
@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE)
|
||||
public class KeycloakController implements Reconciler<Keycloak>, EventSourceInitializer<Keycloak>, ErrorStatusHandler<Keycloak> {
|
||||
|
||||
@Inject
|
||||
KubernetesClient client;
|
||||
|
||||
@Inject
|
||||
Config config;
|
||||
|
||||
@Override
|
||||
public List<EventSource> prepareEventSources(EventSourceContext<Keycloak> context) {
|
||||
SharedIndexInformer<Deployment> deploymentInformer =
|
||||
client.apps().deployments().inAnyNamespace()
|
||||
.withLabels(Constants.DEFAULT_LABELS)
|
||||
.runnableInformer(0);
|
||||
|
||||
EventSource deploymentEvent = new InformerEventSource<>(
|
||||
deploymentInformer, d -> {
|
||||
List<OwnerReference> ownerReferences = d.getMetadata().getOwnerReferences();
|
||||
if (!ownerReferences.isEmpty()) {
|
||||
return Set.of(new ResourceID(ownerReferences.get(0).getName(), d.getMetadata().getNamespace()));
|
||||
} else {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
});
|
||||
|
||||
return List.of(deploymentEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateControl<Keycloak> reconcile(Keycloak kc, Context context) {
|
||||
logger.trace("Reconcile loop started");
|
||||
final var spec = kc.getSpec();
|
||||
String kcName = kc.getMetadata().getName();
|
||||
String namespace = kc.getMetadata().getNamespace();
|
||||
|
||||
logger.info("Reconciling Keycloak: " + kc.getMetadata().getName() + " in namespace: " + kc.getMetadata().getNamespace());
|
||||
Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);
|
||||
|
||||
KeycloakStatus status = kc.getStatus();
|
||||
var deployment = new KeycloakDeployment(client);
|
||||
var statusBuilder = new KeycloakStatusBuilder();
|
||||
|
||||
try {
|
||||
var kcDeployment = deployment.getKeycloakDeployment(kc);
|
||||
// TODO use caches in secondary resources; this is a workaround for https://github.com/java-operator-sdk/java-operator-sdk/issues/830
|
||||
// KeycloakDeployment deployment = new KeycloakDeployment(client, config, kc, context.getSecondaryResource(Deployment.class).orElse(null));
|
||||
var kcDeployment = new KeycloakDeployment(client, config, kc, null);
|
||||
kcDeployment.updateStatus(statusBuilder);
|
||||
kcDeployment.createOrUpdateReconciled();
|
||||
|
||||
if (kcDeployment == null) {
|
||||
// Need to create the deployment
|
||||
deployment.createKeycloakDeployment(kc);
|
||||
}
|
||||
var status = statusBuilder.build();
|
||||
|
||||
var nextStatus = deployment.getNextStatus(spec, status, kcDeployment);
|
||||
Log.info("--- Reconciliation finished successfully");
|
||||
|
||||
if (!nextStatus.equals(status)) {
|
||||
logger.trace("Updating the status");
|
||||
kc.setStatus(nextStatus);
|
||||
return UpdateControl.updateStatus(kc);
|
||||
} else {
|
||||
logger.trace("Nothing to do");
|
||||
if (status.equals(kc.getStatus())) {
|
||||
return UpdateControl.noUpdate();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Error reconciling", e);
|
||||
status = new KeycloakStatus();
|
||||
status.setMessage("Error performing operations:\n" + e.getMessage());
|
||||
status.setState(KeycloakStatus.State.ERROR);
|
||||
status.setError(true);
|
||||
|
||||
else {
|
||||
kc.setStatus(status);
|
||||
return UpdateControl.updateStatus(kc);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Keycloak> updateErrorStatus(Keycloak kc, RetryInfo retryInfo, RuntimeException e) {
|
||||
Log.error("--- Error reconciling", e);
|
||||
KeycloakStatus status = new KeycloakStatusBuilder()
|
||||
.addErrorMessage("Error performing operations:\n" + e.getMessage())
|
||||
.build();
|
||||
|
||||
kc.setStatus(status);
|
||||
|
||||
return Optional.of(kc);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,82 +16,162 @@
|
|||
*/
|
||||
package org.keycloak.operator.v2alpha1;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.Container;
|
||||
import io.fabric8.kubernetes.api.model.EnvVarBuilder;
|
||||
import io.fabric8.kubernetes.api.model.HasMetadata;
|
||||
import io.fabric8.kubernetes.api.model.apps.Deployment;
|
||||
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
|
||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
||||
import io.quarkus.logging.Log;
|
||||
import org.keycloak.operator.Config;
|
||||
import org.keycloak.operator.Constants;
|
||||
import org.keycloak.operator.OperatorManagedResource;
|
||||
import org.keycloak.operator.v2alpha1.crds.Keycloak;
|
||||
import org.keycloak.operator.v2alpha1.crds.KeycloakSpec;
|
||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatus;
|
||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.operator.v2alpha1.crds.KeycloakStatus.State.*;
|
||||
public class KeycloakDeployment extends OperatorManagedResource {
|
||||
|
||||
public class KeycloakDeployment {
|
||||
// public static final Pattern CONFIG_SECRET_PATTERN = Pattern.compile("^\\$\\{secret:([^:]+):(.+)}$");
|
||||
|
||||
KubernetesClient client = null;
|
||||
private final Config config;
|
||||
private final Keycloak keycloakCR;
|
||||
private final Deployment existingDeployment;
|
||||
private final Deployment baseDeployment;
|
||||
|
||||
KeycloakDeployment(KubernetesClient client) {
|
||||
this.client = client;
|
||||
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, Deployment existingDeployment) {
|
||||
super(client, keycloakCR);
|
||||
this.config = config;
|
||||
this.keycloakCR = keycloakCR;
|
||||
|
||||
if (existingDeployment != null) {
|
||||
Log.info("Existing Deployment provided by controller");
|
||||
this.existingDeployment = existingDeployment;
|
||||
}
|
||||
else {
|
||||
Log.info("Trying to fetch existing Deployment from the API");
|
||||
this.existingDeployment = fetchExistingDeployment();
|
||||
}
|
||||
|
||||
private Deployment baseDeployment;
|
||||
baseDeployment = createBaseDeployment();
|
||||
}
|
||||
|
||||
public Deployment getKeycloakDeployment(Keycloak keycloak) {
|
||||
// TODO this should be done through an informer to leverage caches
|
||||
// WORKAROUND for: https://github.com/java-operator-sdk/java-operator-sdk/issues/781
|
||||
@Override
|
||||
protected HasMetadata getReconciledResource() {
|
||||
Deployment baseDeployment = new DeploymentBuilder(this.baseDeployment).build(); // clone not to change the base template
|
||||
Deployment reconciledDeployment;
|
||||
if (existingDeployment == null) {
|
||||
Log.info("No existing Deployment found, using the default");
|
||||
reconciledDeployment = baseDeployment;
|
||||
}
|
||||
else {
|
||||
Log.info("Existing Deployment found, updating specs");
|
||||
reconciledDeployment = existingDeployment;
|
||||
// don't override metadata, just specs
|
||||
reconciledDeployment.setSpec(baseDeployment.getSpec());
|
||||
}
|
||||
|
||||
return reconciledDeployment;
|
||||
}
|
||||
|
||||
private Deployment fetchExistingDeployment() {
|
||||
return client
|
||||
.apps()
|
||||
.deployments()
|
||||
.inNamespace(keycloak.getMetadata().getNamespace())
|
||||
.list()
|
||||
.getItems()
|
||||
.stream()
|
||||
.filter((d) -> d.getMetadata().getName().equals(org.keycloak.operator.Constants.NAME))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
// .withName(Constants.NAME)
|
||||
// .get();
|
||||
.inNamespace(getNamespace())
|
||||
.withName(getName())
|
||||
.get();
|
||||
}
|
||||
|
||||
public void createKeycloakDeployment(Keycloak keycloak) {
|
||||
client
|
||||
.apps()
|
||||
.deployments()
|
||||
.inNamespace(keycloak.getMetadata().getNamespace())
|
||||
.create(newKeycloakDeployment(keycloak));
|
||||
private Deployment createBaseDeployment() {
|
||||
URL url = this.getClass().getResource("/base-keycloak-deployment.yaml");
|
||||
Deployment baseDeployment = client.apps().deployments().load(url).get();
|
||||
|
||||
baseDeployment.getMetadata().setName(getName());
|
||||
baseDeployment.getMetadata().setNamespace(getNamespace());
|
||||
baseDeployment.getSpec().getSelector().setMatchLabels(Constants.DEFAULT_LABELS);
|
||||
baseDeployment.getSpec().setReplicas(keycloakCR.getSpec().getInstances());
|
||||
baseDeployment.getSpec().getTemplate().getMetadata().setLabels(Constants.DEFAULT_LABELS);
|
||||
|
||||
Container container = baseDeployment.getSpec().getTemplate().getSpec().getContainers().get(0);
|
||||
container.setImage(Optional.ofNullable(keycloakCR.getSpec().getImage()).orElse(config.keycloak().image()));
|
||||
|
||||
var serverConfig = new HashMap<>(Constants.DEFAULT_DIST_CONFIG);
|
||||
if (keycloakCR.getSpec().getServerConfiguration() != null) {
|
||||
serverConfig.putAll(keycloakCR.getSpec().getServerConfiguration());
|
||||
}
|
||||
|
||||
public Deployment newKeycloakDeployment(Keycloak keycloak) {
|
||||
if (baseDeployment == null) {
|
||||
URL url = this.getClass().getResource("/base-deployment.yaml");
|
||||
baseDeployment = client.apps().deployments().load(url).get();
|
||||
container.setEnv(serverConfig.entrySet().stream()
|
||||
.map(e -> new EnvVarBuilder().withName(e.getKey()).withValue(e.getValue()).build())
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
// Set<String> configSecretsNames = new HashSet<>();
|
||||
// List<EnvVar> configEnvVars = serverConfig.entrySet().stream()
|
||||
// .map(e -> {
|
||||
// EnvVarBuilder builder = new EnvVarBuilder().withName(e.getKey());
|
||||
// Matcher matcher = CONFIG_SECRET_PATTERN.matcher(e.getValue());
|
||||
// // check if given config var is actually a secret reference
|
||||
// if (matcher.matches()) {
|
||||
// builder.withValueFrom(
|
||||
// new EnvVarSourceBuilder()
|
||||
// .withNewSecretKeyRef(matcher.group(2), matcher.group(1), false)
|
||||
// .build());
|
||||
// configSecretsNames.add(matcher.group(1)); // for watching it later
|
||||
// } else {
|
||||
// builder.withValue(e.getValue());
|
||||
// }
|
||||
// builder.withValue(e.getValue());
|
||||
// return builder.build();
|
||||
// })
|
||||
// .collect(Collectors.toList());
|
||||
// container.setEnv(configEnvVars);
|
||||
// this.configSecretsNames = Collections.unmodifiableSet(configSecretsNames);
|
||||
// Log.infof("Found config secrets names: %s", configSecretsNames);
|
||||
|
||||
return baseDeployment;
|
||||
}
|
||||
|
||||
var deployment = baseDeployment;
|
||||
|
||||
deployment
|
||||
.getSpec()
|
||||
.setReplicas(keycloak.getSpec().getInstances());
|
||||
|
||||
return new DeploymentBuilder(deployment).build();
|
||||
public void updateStatus(KeycloakStatusBuilder status) {
|
||||
if (existingDeployment == null) {
|
||||
status.addNotReadyMessage("No existing Deployment found, waiting for creating a new one");
|
||||
return;
|
||||
}
|
||||
|
||||
public KeycloakStatus getNextStatus(KeycloakSpec desired, KeycloakStatus prev, Deployment current) {
|
||||
var isReady = (current != null &&
|
||||
current.getStatus() != null &&
|
||||
current.getStatus().getReadyReplicas() != null &&
|
||||
current.getStatus().getReadyReplicas() == desired.getInstances());
|
||||
|
||||
var newStatus = new KeycloakStatus();
|
||||
if (isReady) {
|
||||
newStatus.setState(UNKNOWN);
|
||||
newStatus.setMessage("Keycloak status is unmanaged");
|
||||
} else {
|
||||
newStatus.setState(READY);
|
||||
newStatus.setMessage("Keycloak status is ready");
|
||||
}
|
||||
return newStatus;
|
||||
var replicaFailure = existingDeployment.getStatus().getConditions().stream()
|
||||
.filter(d -> d.getType().equals("ReplicaFailure")).findFirst();
|
||||
if (replicaFailure.isPresent()) {
|
||||
status.addNotReadyMessage("Deployment failures");
|
||||
status.addErrorMessage("Deployment failure: " + replicaFailure.get());
|
||||
return;
|
||||
}
|
||||
|
||||
if (existingDeployment.getStatus() == null
|
||||
|| existingDeployment.getStatus().getReadyReplicas() == null
|
||||
|| existingDeployment.getStatus().getReadyReplicas() < keycloakCR.getSpec().getInstances()) {
|
||||
status.addNotReadyMessage("Waiting for more replicas");
|
||||
}
|
||||
}
|
||||
|
||||
// public Set<String> getConfigSecretsNames() {
|
||||
// return configSecretsNames;
|
||||
// }
|
||||
|
||||
public String getName() {
|
||||
return keycloakCR.getMetadata().getName();
|
||||
}
|
||||
|
||||
public String getNamespace() {
|
||||
return keycloakCR.getMetadata().getNamespace();
|
||||
}
|
||||
|
||||
public void rollingRestart() {
|
||||
client.apps().deployments()
|
||||
.inNamespace(getNamespace())
|
||||
.withName(getName())
|
||||
.rolling().restart();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,15 @@
|
|||
*/
|
||||
package org.keycloak.operator.v2alpha1.crds;
|
||||
|
||||
import io.fabric8.kubernetes.api.model.PodTemplate;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class KeycloakSpec {
|
||||
|
||||
private int instances = 1;
|
||||
private String image;
|
||||
private Map<String, String> serverConfiguration;
|
||||
|
||||
public int getInstances() {
|
||||
return instances;
|
||||
|
@ -27,4 +33,20 @@ public class KeycloakSpec {
|
|||
public void setInstances(int instances) {
|
||||
this.instances = instances;
|
||||
}
|
||||
|
||||
public String getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public void setImage(String image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public Map<String, String> getServerConfiguration() {
|
||||
return serverConfiguration;
|
||||
}
|
||||
|
||||
public void setServerConfiguration(Map<String, String> serverConfiguration) {
|
||||
this.serverConfiguration = serverConfiguration;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,46 +16,33 @@
|
|||
*/
|
||||
package org.keycloak.operator.v2alpha1.crds;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class KeycloakStatus {
|
||||
public enum State {
|
||||
READY,
|
||||
ERROR,
|
||||
UNKNOWN
|
||||
private List<KeycloakStatusCondition> conditions;
|
||||
|
||||
public List<KeycloakStatusCondition> getConditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
private State state = State.UNKNOWN;
|
||||
private boolean error;
|
||||
private String message;
|
||||
|
||||
public State getState() {
|
||||
return state;
|
||||
public void setConditions(List<KeycloakStatusCondition> conditions) {
|
||||
this.conditions = conditions;
|
||||
}
|
||||
|
||||
public void setState(State state) {
|
||||
this.state = state;
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
KeycloakStatus status = (KeycloakStatus) o;
|
||||
return Objects.equals(getConditions(), status.getConditions());
|
||||
}
|
||||
|
||||
public boolean isError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
public void setError(boolean error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public KeycloakStatus clone() {
|
||||
var status = new KeycloakStatus();
|
||||
status.setMessage(this.message);
|
||||
status.setState(this.state);
|
||||
status.setError(this.error);
|
||||
return status;
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(getConditions());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class KeycloakStatusBuilder {
|
||||
private final KeycloakStatusCondition readyCondition;
|
||||
private final KeycloakStatusCondition hasErrorsCondition;
|
||||
|
||||
private final List<String> notReadyMessages = new ArrayList<>();
|
||||
private final List<String> errorMessages = new ArrayList<>();
|
||||
|
||||
public KeycloakStatusBuilder() {
|
||||
readyCondition = new KeycloakStatusCondition();
|
||||
readyCondition.setType(KeycloakStatusCondition.READY);
|
||||
readyCondition.setStatus(true);
|
||||
|
||||
hasErrorsCondition = new KeycloakStatusCondition();
|
||||
hasErrorsCondition.setType(KeycloakStatusCondition.HAS_ERRORS);
|
||||
hasErrorsCondition.setStatus(false);
|
||||
}
|
||||
|
||||
public KeycloakStatusBuilder addNotReadyMessage(String message) {
|
||||
readyCondition.setStatus(false);
|
||||
notReadyMessages.add(message);
|
||||
return this;
|
||||
}
|
||||
|
||||
public KeycloakStatusBuilder addErrorMessage(String message) {
|
||||
hasErrorsCondition.setStatus(true);
|
||||
errorMessages.add(message);
|
||||
return this;
|
||||
}
|
||||
|
||||
public KeycloakStatus build() {
|
||||
readyCondition.setMessage(String.join("\n", notReadyMessages));
|
||||
hasErrorsCondition.setMessage(String.join("\n", errorMessages));
|
||||
|
||||
KeycloakStatus status = new KeycloakStatus();
|
||||
status.setConditions(List.of(readyCondition, hasErrorsCondition));
|
||||
return status;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.io.Serializable;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||
*/
|
||||
public class KeycloakStatusCondition {
|
||||
public static final String READY = "Ready";
|
||||
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;
|
||||
KeycloakStatusCondition that = (KeycloakStatusCondition) 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());
|
||||
}
|
||||
}
|
|
@ -2,3 +2,6 @@ quarkus.operator-sdk.crd.apply=true
|
|||
quarkus.operator-sdk.generate-csv=true
|
||||
quarkus.container-image.builder=jib
|
||||
quarkus.operator-sdk.crd.validate=false
|
||||
|
||||
# Operator config
|
||||
operator.keycloak.image=quay.io/keycloak/keycloak-x:latest
|
|
@ -1,15 +1,11 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: keycloak-operator
|
||||
name: keycloak
|
||||
namespace: default
|
||||
name: ""
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: keycloak
|
||||
app: ""
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
|
@ -18,12 +14,11 @@ spec:
|
|||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: keycloak
|
||||
app: ""
|
||||
spec:
|
||||
containers:
|
||||
- args:
|
||||
- start-dev
|
||||
image: quay.io/keycloak/keycloak-x:latest
|
||||
imagePullPolicy: Always
|
||||
name: keycloak
|
||||
ports:
|
||||
|
@ -31,10 +26,17 @@ spec:
|
|||
protocol: TCP
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- curl --head --fail --silent http://127.0.0.1:8080/health/live
|
||||
periodSeconds: 1
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- curl --head --fail --silent http://127.0.0.1:8080/health/ready
|
||||
periodSeconds: 1
|
||||
failureThreshold: 180
|
||||
dnsPolicy: ClusterFirst
|
||||
initContainers:
|
||||
- image: quay.io/keycloak/keycloak-init-container:latest
|
||||
imagePullPolicy: Always
|
||||
name: init-container
|
||||
restartPolicy: Always
|
||||
terminationGracePeriodSeconds: 30
|
|
@ -1,6 +1,22 @@
|
|||
apiVersion: keycloak.io/v2alpha1
|
||||
apiVersion: keycloak.org/v2alpha1
|
||||
kind: Keycloak
|
||||
metadata:
|
||||
name: example-kc
|
||||
spec:
|
||||
instances: 1
|
||||
distConfig:
|
||||
KC_DB: postgres
|
||||
KC_DB_URL_HOST: postgres-db
|
||||
# KC_DB_USERNAME: ${secret:keycloak-db-secret:username}
|
||||
# KC_DB_PASSWORD: ${secret:keycloak-db-secret:password}
|
||||
KC_DB_USERNAME: postgres
|
||||
KC_DB_PASSWORD: testpassword
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: keycloak-db-secret
|
||||
data:
|
||||
username: cG9zdGdyZXM= # postgres
|
||||
password: dGVzdHBhc3N3b3Jk # testpassword
|
||||
type: Opaque
|
39
operator/src/main/resources/example-postgres.yaml
Normal file
39
operator/src/main/resources/example-postgres.yaml
Normal file
|
@ -0,0 +1,39 @@
|
|||
# PostgreSQL StatefulSet
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgresql-db
|
||||
spec:
|
||||
serviceName: postgresql-db-service
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgresql-db
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgresql-db
|
||||
spec:
|
||||
containers:
|
||||
- name: postgresql-db
|
||||
image: postgres:latest
|
||||
env:
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: testpassword
|
||||
- name: PGDATA
|
||||
value: /data/pgdata
|
||||
- name: POSTGRES_DB
|
||||
value: keycloak
|
||||
---
|
||||
# PostgreSQL StatefulSet Service
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgres-db
|
||||
spec:
|
||||
selector:
|
||||
app: postgresql-db
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
Loading…
Reference in a new issue