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 String MANAGED_BY_VALUE = "keycloak-operator";
|
||||||
|
|
||||||
public static final Map<String, String> DEFAULT_LABELS = Map.of(
|
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 Map<String, String> DEFAULT_DIST_CONFIG = Map.of(
|
||||||
public static final String DEFAULT_KEYCLOAK_INIT_IMAGE = "quay.io/keycloak/keycloak-init-container:latest";
|
"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 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.fabric8.kubernetes.client.KubernetesClient;
|
||||||
import io.javaoperatorsdk.operator.api.reconciler.*;
|
import io.fabric8.kubernetes.client.informers.SharedIndexInformer;
|
||||||
import io.javaoperatorsdk.operator.api.reconciler.Constants;
|
import io.javaoperatorsdk.operator.api.reconciler.Context;
|
||||||
import org.jboss.logging.Logger;
|
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.Keycloak;
|
||||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatus;
|
import org.keycloak.operator.v2alpha1.crds.KeycloakStatus;
|
||||||
|
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder;
|
||||||
|
|
||||||
@ControllerConfiguration(namespaces = Constants.WATCH_CURRENT_NAMESPACE, finalizerName = Constants.NO_FINALIZER)
|
import java.util.Collections;
|
||||||
public class KeycloakController implements Reconciler<Keycloak> {
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Inject
|
import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE;
|
||||||
Logger logger;
|
|
||||||
|
@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE)
|
||||||
|
public class KeycloakController implements Reconciler<Keycloak>, EventSourceInitializer<Keycloak>, ErrorStatusHandler<Keycloak> {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
KubernetesClient client;
|
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
|
@Override
|
||||||
public UpdateControl<Keycloak> reconcile(Keycloak kc, Context context) {
|
public UpdateControl<Keycloak> reconcile(Keycloak kc, Context context) {
|
||||||
logger.trace("Reconcile loop started");
|
String kcName = kc.getMetadata().getName();
|
||||||
final var spec = kc.getSpec();
|
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 statusBuilder = new KeycloakStatusBuilder();
|
||||||
var deployment = new KeycloakDeployment(client);
|
|
||||||
|
|
||||||
try {
|
// TODO use caches in secondary resources; this is a workaround for https://github.com/java-operator-sdk/java-operator-sdk/issues/830
|
||||||
var kcDeployment = deployment.getKeycloakDeployment(kc);
|
// 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) {
|
var status = statusBuilder.build();
|
||||||
// Need to create the deployment
|
|
||||||
deployment.createKeycloakDeployment(kc);
|
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
if (status.equals(kc.getStatus())) {
|
||||||
|
return UpdateControl.noUpdate();
|
||||||
|
}
|
||||||
|
else {
|
||||||
kc.setStatus(status);
|
kc.setStatus(status);
|
||||||
return UpdateControl.updateStatus(kc);
|
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;
|
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.Deployment;
|
||||||
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
|
import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
|
||||||
import io.fabric8.kubernetes.client.KubernetesClient;
|
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.Keycloak;
|
||||||
import org.keycloak.operator.v2alpha1.crds.KeycloakSpec;
|
import org.keycloak.operator.v2alpha1.crds.KeycloakStatusBuilder;
|
||||||
import org.keycloak.operator.v2alpha1.crds.KeycloakStatus;
|
|
||||||
|
|
||||||
import java.net.URL;
|
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) {
|
public KeycloakDeployment(KubernetesClient client, Config config, Keycloak keycloakCR, Deployment existingDeployment) {
|
||||||
this.client = client;
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDeployment = createBaseDeployment();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Deployment baseDeployment;
|
@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());
|
||||||
|
}
|
||||||
|
|
||||||
public Deployment getKeycloakDeployment(Keycloak keycloak) {
|
return reconciledDeployment;
|
||||||
// TODO this should be done through an informer to leverage caches
|
}
|
||||||
// WORKAROUND for: https://github.com/java-operator-sdk/java-operator-sdk/issues/781
|
|
||||||
|
private Deployment fetchExistingDeployment() {
|
||||||
return client
|
return client
|
||||||
.apps()
|
.apps()
|
||||||
.deployments()
|
.deployments()
|
||||||
.inNamespace(keycloak.getMetadata().getNamespace())
|
.inNamespace(getNamespace())
|
||||||
.list()
|
.withName(getName())
|
||||||
.getItems()
|
.get();
|
||||||
.stream()
|
|
||||||
.filter((d) -> d.getMetadata().getName().equals(org.keycloak.operator.Constants.NAME))
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null);
|
|
||||||
// .withName(Constants.NAME)
|
|
||||||
// .get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createKeycloakDeployment(Keycloak keycloak) {
|
private Deployment createBaseDeployment() {
|
||||||
client
|
URL url = this.getClass().getResource("/base-keycloak-deployment.yaml");
|
||||||
.apps()
|
Deployment baseDeployment = client.apps().deployments().load(url).get();
|
||||||
.deployments()
|
|
||||||
.inNamespace(keycloak.getMetadata().getNamespace())
|
|
||||||
.create(newKeycloakDeployment(keycloak));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Deployment newKeycloakDeployment(Keycloak keycloak) {
|
baseDeployment.getMetadata().setName(getName());
|
||||||
if (baseDeployment == null) {
|
baseDeployment.getMetadata().setNamespace(getNamespace());
|
||||||
URL url = this.getClass().getResource("/base-deployment.yaml");
|
baseDeployment.getSpec().getSelector().setMatchLabels(Constants.DEFAULT_LABELS);
|
||||||
baseDeployment = client.apps().deployments().load(url).get();
|
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());
|
||||||
}
|
}
|
||||||
|
|
||||||
var deployment = baseDeployment;
|
container.setEnv(serverConfig.entrySet().stream()
|
||||||
|
.map(e -> new EnvVarBuilder().withName(e.getKey()).withValue(e.getValue()).build())
|
||||||
|
.collect(Collectors.toList()));
|
||||||
|
|
||||||
deployment
|
// Set<String> configSecretsNames = new HashSet<>();
|
||||||
.getSpec()
|
// List<EnvVar> configEnvVars = serverConfig.entrySet().stream()
|
||||||
.setReplicas(keycloak.getSpec().getInstances());
|
// .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 new DeploymentBuilder(deployment).build();
|
return baseDeployment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public KeycloakStatus getNextStatus(KeycloakSpec desired, KeycloakStatus prev, Deployment current) {
|
public void updateStatus(KeycloakStatusBuilder status) {
|
||||||
var isReady = (current != null &&
|
if (existingDeployment == null) {
|
||||||
current.getStatus() != null &&
|
status.addNotReadyMessage("No existing Deployment found, waiting for creating a new one");
|
||||||
current.getStatus().getReadyReplicas() != null &&
|
return;
|
||||||
current.getStatus().getReadyReplicas() == desired.getInstances());
|
}
|
||||||
|
|
||||||
var newStatus = new KeycloakStatus();
|
var replicaFailure = existingDeployment.getStatus().getConditions().stream()
|
||||||
if (isReady) {
|
.filter(d -> d.getType().equals("ReplicaFailure")).findFirst();
|
||||||
newStatus.setState(UNKNOWN);
|
if (replicaFailure.isPresent()) {
|
||||||
newStatus.setMessage("Keycloak status is unmanaged");
|
status.addNotReadyMessage("Deployment failures");
|
||||||
} else {
|
status.addErrorMessage("Deployment failure: " + replicaFailure.get());
|
||||||
newStatus.setState(READY);
|
return;
|
||||||
newStatus.setMessage("Keycloak status is ready");
|
}
|
||||||
|
|
||||||
|
if (existingDeployment.getStatus() == null
|
||||||
|
|| existingDeployment.getStatus().getReadyReplicas() == null
|
||||||
|
|| existingDeployment.getStatus().getReadyReplicas() < keycloakCR.getSpec().getInstances()) {
|
||||||
|
status.addNotReadyMessage("Waiting for more replicas");
|
||||||
}
|
}
|
||||||
return newStatus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
package org.keycloak.operator.v2alpha1.crds;
|
||||||
|
|
||||||
|
import io.fabric8.kubernetes.api.model.PodTemplate;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class KeycloakSpec {
|
public class KeycloakSpec {
|
||||||
|
|
||||||
private int instances = 1;
|
private int instances = 1;
|
||||||
|
private String image;
|
||||||
|
private Map<String, String> serverConfiguration;
|
||||||
|
|
||||||
public int getInstances() {
|
public int getInstances() {
|
||||||
return instances;
|
return instances;
|
||||||
|
@ -27,4 +33,20 @@ public class KeycloakSpec {
|
||||||
public void setInstances(int instances) {
|
public void setInstances(int instances) {
|
||||||
this.instances = 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;
|
package org.keycloak.operator.v2alpha1.crds;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Vaclav Muzikar <vmuzikar@redhat.com>
|
||||||
|
*/
|
||||||
public class KeycloakStatus {
|
public class KeycloakStatus {
|
||||||
public enum State {
|
private List<KeycloakStatusCondition> conditions;
|
||||||
READY,
|
|
||||||
ERROR,
|
public List<KeycloakStatusCondition> getConditions() {
|
||||||
UNKNOWN
|
return conditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private State state = State.UNKNOWN;
|
public void setConditions(List<KeycloakStatusCondition> conditions) {
|
||||||
private boolean error;
|
this.conditions = conditions;
|
||||||
private String message;
|
|
||||||
|
|
||||||
public State getState() {
|
|
||||||
return state;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setState(State state) {
|
@Override
|
||||||
this.state = state;
|
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() {
|
@Override
|
||||||
return error;
|
public int hashCode() {
|
||||||
}
|
return Objects.hash(getConditions());
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.operator-sdk.generate-csv=true
|
||||||
quarkus.container-image.builder=jib
|
quarkus.container-image.builder=jib
|
||||||
quarkus.operator-sdk.crd.validate=false
|
quarkus.operator-sdk.crd.validate=false
|
||||||
|
|
||||||
|
# Operator config
|
||||||
|
operator.keycloak.image=quay.io/keycloak/keycloak-x:latest
|
|
@ -1,15 +1,11 @@
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
name: ""
|
||||||
app.kubernetes.io/managed-by: keycloak-operator
|
|
||||||
name: keycloak
|
|
||||||
namespace: default
|
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: keycloak
|
app: ""
|
||||||
strategy:
|
strategy:
|
||||||
rollingUpdate:
|
rollingUpdate:
|
||||||
maxSurge: 25%
|
maxSurge: 25%
|
||||||
|
@ -18,12 +14,11 @@ spec:
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: keycloak
|
app: ""
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- args:
|
- args:
|
||||||
- start-dev
|
- start-dev
|
||||||
image: quay.io/keycloak/keycloak-x:latest
|
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
name: keycloak
|
name: keycloak
|
||||||
ports:
|
ports:
|
||||||
|
@ -31,10 +26,17 @@ spec:
|
||||||
protocol: TCP
|
protocol: TCP
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
protocol: TCP
|
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
|
dnsPolicy: ClusterFirst
|
||||||
initContainers:
|
|
||||||
- image: quay.io/keycloak/keycloak-init-container:latest
|
|
||||||
imagePullPolicy: Always
|
|
||||||
name: init-container
|
|
||||||
restartPolicy: Always
|
restartPolicy: Always
|
||||||
terminationGracePeriodSeconds: 30
|
terminationGracePeriodSeconds: 30
|
|
@ -1,6 +1,22 @@
|
||||||
apiVersion: keycloak.io/v2alpha1
|
apiVersion: keycloak.org/v2alpha1
|
||||||
kind: Keycloak
|
kind: Keycloak
|
||||||
metadata:
|
metadata:
|
||||||
name: example-kc
|
name: example-kc
|
||||||
spec:
|
spec:
|
||||||
instances: 1
|
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