Baseline for Keycloak deployment in operator

This commit is contained in:
Václav Muzikář 2022-01-11 14:36:43 +01:00 committed by Pedro Igor
parent d28b54e5d5
commit 6b485b8603
13 changed files with 573 additions and 134 deletions

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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