diff --git a/.github/workflows/operator-ci.yml b/.github/workflows/operator-ci.yml
new file mode 100644
index 0000000000..554f82c354
--- /dev/null
+++ b/.github/workflows/operator-ci.yml
@@ -0,0 +1,39 @@
+name: Keycloak Operator CI
+
+on: [push, pull_request]
+
+env:
+ JDK_VERSION: 11
+
+concurrency:
+ # Only run once for latest commit per ref and cancel other (previous) runs.
+ group: ci-operator-keycloak-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Update maven settings
+ run: mkdir -p ~/.m2 ; cp .github/settings.xml ~/.m2/
+ - uses: actions/setup-java@v1
+ with:
+ java-version: ${{ env.JDK_VERSION }}
+ - name: Cache Maven packages
+ id: cache
+ uses: actions/cache@v2
+ with:
+ path: |
+ ~/.m2/repository
+ key: cache-1-${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
+ restore-keys: cache-1-${{ runner.os }}-m2
+
+ - name: Create the Keycloak distribution
+ run: |
+ mvn clean install -DskipTests -DskipExamples -DskipTestsuite
+
+ - name: Build the Keycloak Operator
+ run: |
+ mvn clean package -nsu -B -e -pl operator -Doperator -Dquarkus.container-image.build=true -Dquarkus.kubernetes.deployment-target=minikube
diff --git a/operator/README.md b/operator/README.md
new file mode 100644
index 0000000000..866a3dbab9
--- /dev/null
+++ b/operator/README.md
@@ -0,0 +1,40 @@
+# Keycloak on Quarkus
+
+The module holds the codebase to build the Keycloak Operator on top of [Quarkus](https://quarkus.io/).
+Using the [Quarkus Operator SDK](https://github.com/quarkiverse/quarkus-operator-sdk).
+
+## Activating the Module
+
+When build from the project root directory, this module is only enabled if the installed JDK is 11 or newer.
+
+## Building
+
+Ensure you have JDK 11 (or newer) installed.
+
+Build the Docker image with:
+
+```bash
+mvn clean package -Doperator -Dquarkus.container-image.build=true
+```
+
+## Contributing
+
+### Quick start on Minikube
+
+Enable the Minikube Docker daemon:
+
+```bash
+eval $(minikube -p minikube docker-env)
+```
+
+Compile the project and generate the Docker image with JIB:
+
+```bash
+mvn clean package -Doperator -Dquarkus.container-image.build=true -Dquarkus.kubernetes.deployment-target=minikube
+```
+
+Install the CRD definition and the operator in the cluster:
+
+```bash
+kubectl apply -k .
+```
diff --git a/operator/kubernetes/deployments-role.yaml b/operator/kubernetes/deployments-role.yaml
new file mode 100644
index 0000000000..ed480562b9
--- /dev/null
+++ b/operator/kubernetes/deployments-role.yaml
@@ -0,0 +1,31 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: keycloak-operator-role
+rules:
+ - apiGroups:
+ - apps
+ resources:
+ - deployments
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - delete
+ - patch
+ - update
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ labels:
+ app.kubernetes.io/name: keycloak-operator
+ name: keycloak-operator-role-binding
+roleRef:
+ kind: Role
+ apiGroup: rbac.authorization.k8s.io
+ name: keycloak-operator-role
+subjects:
+ - kind: ServiceAccount
+ name: keycloak-operator
diff --git a/operator/kustomization.yaml b/operator/kustomization.yaml
new file mode 100644
index 0000000000..8da405279a
--- /dev/null
+++ b/operator/kustomization.yaml
@@ -0,0 +1,4 @@
+resources:
+ - target/kubernetes/keycloaks.keycloak.org-v1.yml
+ - target/kubernetes/minikube.yml
+ - kubernetes/deployments-role.yaml
diff --git a/operator/pom.xml b/operator/pom.xml
new file mode 100644
index 0000000000..0d672d5d67
--- /dev/null
+++ b/operator/pom.xml
@@ -0,0 +1,149 @@
+
+
+ 4.0.0
+
+
+ keycloak-parent
+ org.keycloak
+ 17.0.0-SNAPSHOT
+
+
+ Keycloak Operator
+ keycloak-operator
+
+
+
+ 4.7.4.Final
+ 1.5.4.Final-format-001
+
+ 3.8.1
+ true
+ 11
+ 11
+ 11
+ UTF-8
+ UTF-8
+ 3.0.0-SNAPSHOT
+ 2.6.1.Final
+ keycloak
+ eclipse-temurin:11
+ Never
+
+
+
+
+ s01.oss.sonatype
+ https://s01.oss.sonatype.org/content/repositories/snapshots/
+
+
+
+
+
+
+ io.quarkiverse.operatorsdk
+ quarkus-operator-sdk-bom
+ ${quarkus.operator.sdk.version}
+ pom
+ import
+
+
+
+
+
+
+
+
+ io.quarkiverse.operatorsdk
+ quarkus-operator-sdk
+
+
+ io.quarkiverse.operatorsdk
+ quarkus-operator-sdk-csv-generator
+
+
+ io.quarkus
+ quarkus-resteasy-jackson
+
+
+ io.quarkus
+ quarkus-rest-client
+
+
+ io.quarkus
+ quarkus-rest-client-jackson
+
+
+ io.quarkus
+ quarkus-openshift
+
+
+ io.quarkus
+ quarkus-minikube
+
+
+ io.quarkus
+ quarkus-kubernetes-client
+
+
+
+
+
+ io.quarkiverse.operatorsdk
+ quarkus-operator-sdk-csv-generator-deployment
+ provided
+
+
+
+
+ org.keycloak
+ keycloak-core
+
+
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${quarkus.version}
+
+
+ maven-compiler-plugin
+ ${compiler-plugin.version}
+
+
+
+
+
+ io.quarkus
+ quarkus-maven-plugin
+ ${quarkus.version}
+
+
+
+ build
+
+
+
+
+
+
+
+
+
+
+ native
+
+ native
+
+
+
+
diff --git a/operator/src/main/java/org/keycloak/operator/Constants.java b/operator/src/main/java/org/keycloak/operator/Constants.java
new file mode 100644
index 0000000000..210821ab14
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/Constants.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.operator;
+
+import java.util.Map;
+
+public final class Constants {
+ public static final String CRDS_GROUP = "keycloak.org";
+ public static final String CRDS_VERSION = "v2alpha1";
+ public static final String SHORT_NAME = "kc";
+ public static final String NAME = "keycloak";
+ public static final String PLURAL_NAME = "keycloaks";
+ public static final String MANAGED_BY_LABEL = "app.kubernetes.io/managed-by";
+ public static final String MANAGED_BY_VALUE = "keycloak-operator";
+
+ public static final Map DEFAULT_LABELS = Map.of(
+ "app", NAME
+ );
+
+ 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";
+}
diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakController.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakController.java
new file mode 100644
index 0000000000..0ef1d0cf59
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakController.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.operator.v2alpha1;
+
+import javax.inject.Inject;
+
+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 org.keycloak.operator.v2alpha1.crds.Keycloak;
+import org.keycloak.operator.v2alpha1.crds.KeycloakStatus;
+
+@ControllerConfiguration(namespaces = Constants.WATCH_CURRENT_NAMESPACE, finalizerName = Constants.NO_FINALIZER)
+public class KeycloakController implements Reconciler {
+
+ @Inject
+ Logger logger;
+
+ @Inject
+ KubernetesClient client;
+
+ @Override
+ public UpdateControl 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);
+
+ kc.setStatus(status);
+ return UpdateControl.updateStatus(kc);
+ }
+ }
+}
diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java
new file mode 100644
index 0000000000..e0e0faa6c5
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/KeycloakDeployment.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.operator.v2alpha1;
+
+import io.fabric8.kubernetes.api.model.apps.Deployment;
+import io.fabric8.kubernetes.api.model.apps.DeploymentBuilder;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import org.keycloak.operator.v2alpha1.crds.Keycloak;
+import org.keycloak.operator.v2alpha1.crds.KeycloakSpec;
+import org.keycloak.operator.v2alpha1.crds.KeycloakStatus;
+
+import java.net.URL;
+
+import static org.keycloak.operator.v2alpha1.crds.KeycloakStatus.State.*;
+
+public class KeycloakDeployment {
+
+ KubernetesClient client = null;
+
+ KeycloakDeployment(KubernetesClient client) {
+ this.client = client;
+ }
+
+ private Deployment baseDeployment;
+
+ 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
+ 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();
+ }
+
+ public void createKeycloakDeployment(Keycloak keycloak) {
+ client
+ .apps()
+ .deployments()
+ .inNamespace(keycloak.getMetadata().getNamespace())
+ .create(newKeycloakDeployment(keycloak));
+ }
+
+ public Deployment newKeycloakDeployment(Keycloak keycloak) {
+ if (baseDeployment == null) {
+ URL url = this.getClass().getResource("/base-deployment.yaml");
+ baseDeployment = client.apps().deployments().load(url).get();
+ }
+
+ var deployment = baseDeployment;
+
+ deployment
+ .getSpec()
+ .setReplicas(keycloak.getSpec().getInstances());
+
+ return new DeploymentBuilder(deployment).build();
+ }
+
+ 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;
+ }
+
+}
diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/Keycloak.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/Keycloak.java
new file mode 100644
index 0000000000..291cef145e
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/Keycloak.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.operator.v2alpha1.crds;
+
+import io.fabric8.kubernetes.api.model.Namespaced;
+import io.fabric8.kubernetes.client.CustomResource;
+import io.fabric8.kubernetes.model.annotation.Group;
+import io.fabric8.kubernetes.model.annotation.Plural;
+import io.fabric8.kubernetes.model.annotation.ShortNames;
+import io.fabric8.kubernetes.model.annotation.Version;
+import org.keycloak.operator.Constants;
+
+@Group(Constants.CRDS_GROUP)
+@Version(Constants.CRDS_VERSION)
+@ShortNames(Constants.SHORT_NAME)
+@Plural(Constants.PLURAL_NAME)
+public class Keycloak extends CustomResource implements Namespaced {
+
+}
diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java
new file mode 100644
index 0000000000..47655f91c8
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakSpec.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.operator.v2alpha1.crds;
+
+public class KeycloakSpec {
+
+ private int instances = 1;
+
+ public int getInstances() {
+ return instances;
+ }
+
+ public void setInstances(int instances) {
+ this.instances = instances;
+ }
+}
diff --git a/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakStatus.java b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakStatus.java
new file mode 100644
index 0000000000..aa171eeb44
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/v2alpha1/crds/KeycloakStatus.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2021 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.operator.v2alpha1.crds;
+
+public class KeycloakStatus {
+ public enum State {
+ READY,
+ ERROR,
+ UNKNOWN
+ }
+
+ private State state = State.UNKNOWN;
+ private boolean error;
+ private String message;
+
+ public State getState() {
+ return state;
+ }
+
+ public void setState(State state) {
+ this.state = state;
+ }
+
+ 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;
+ }
+}
diff --git a/operator/src/main/resources/application.properties b/operator/src/main/resources/application.properties
new file mode 100644
index 0000000000..66c98a4359
--- /dev/null
+++ b/operator/src/main/resources/application.properties
@@ -0,0 +1,4 @@
+quarkus.operator-sdk.crd.apply=true
+quarkus.operator-sdk.generate-csv=true
+quarkus.container-image.builder=jib
+quarkus.operator-sdk.crd.validate=false
diff --git a/operator/src/main/resources/base-deployment.yaml b/operator/src/main/resources/base-deployment.yaml
new file mode 100644
index 0000000000..34f9096bcd
--- /dev/null
+++ b/operator/src/main/resources/base-deployment.yaml
@@ -0,0 +1,40 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ app.kubernetes.io/managed-by: keycloak-operator
+ name: keycloak
+ namespace: default
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: keycloak
+ strategy:
+ rollingUpdate:
+ maxSurge: 25%
+ maxUnavailable: 25%
+ type: RollingUpdate
+ template:
+ metadata:
+ labels:
+ app: keycloak
+ spec:
+ containers:
+ - args:
+ - start-dev
+ image: quay.io/keycloak/keycloak-x:latest
+ imagePullPolicy: Always
+ name: keycloak
+ ports:
+ - containerPort: 8443
+ protocol: TCP
+ - containerPort: 8080
+ protocol: TCP
+ dnsPolicy: ClusterFirst
+ initContainers:
+ - image: quay.io/keycloak/keycloak-init-container:latest
+ imagePullPolicy: Always
+ name: init-container
+ restartPolicy: Always
+ terminationGracePeriodSeconds: 30
diff --git a/operator/src/main/resources/example-keycloak.yml b/operator/src/main/resources/example-keycloak.yml
new file mode 100644
index 0000000000..da57ab6008
--- /dev/null
+++ b/operator/src/main/resources/example-keycloak.yml
@@ -0,0 +1,6 @@
+apiVersion: keycloak.io/v2alpha1
+kind: Keycloak
+metadata:
+ name: example-kc
+spec:
+ instances: 1
diff --git a/pom.xml b/pom.xml
index 0415c4c18d..9a6684889f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2040,6 +2040,19 @@
+
+ operator
+
+ [11,)
+
+ operator
+
+
+
+ operator
+
+
+
doclint-java8-disable