Skip to content

Commit

Permalink
Baseline for Keycloak deployment in operator
Browse files Browse the repository at this point in the history
  • Loading branch information
vmuzikar authored and pedroigor committed Jan 25, 2022
1 parent d28b54e commit 6b485b8
Show file tree
Hide file tree
Showing 13 changed files with 577 additions and 138 deletions.
32 changes: 32 additions & 0 deletions operator/src/main/java/org/keycloak/operator/Config.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
8 changes: 5 additions & 3 deletions operator/src/main/java/org/keycloak/operator/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"
);
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,59 +18,101 @@

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

logger.info("Reconciling Keycloak: " + kc.getMetadata().getName() + " in namespace: " + kc.getMetadata().getNamespace());

KeycloakStatus status = kc.getStatus();
var deployment = new KeycloakDeployment(client);

try {
var kcDeployment = deployment.getKeycloakDeployment(kc);

if (kcDeployment == null) {
// Need to create the deployment
deployment.createKeycloakDeployment(kc);
}

var nextStatus = deployment.getNextStatus(spec, status, kcDeployment);

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);
String kcName = kc.getMetadata().getName();
String namespace = kc.getMetadata().getNamespace();

Log.infof("--- Reconciling Keycloak: %s in namespace: %s", kcName, namespace);

var statusBuilder = new KeycloakStatusBuilder();

// 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();

var status = statusBuilder.build();

Log.info("--- Reconciliation finished successfully");

if (status.equals(kc.getStatus())) {
return UpdateControl.noUpdate();
}
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);
}
}

0 comments on commit 6b485b8

Please sign in to comment.