diff --git a/generated.tf b/generated.tf index 5914ca004d..8c971ecfe7 100644 --- a/generated.tf +++ b/generated.tf @@ -610,6 +610,11 @@ module "keycloak" { target_repository = "${var.target_repository}/keycloak" } +module "keycloak-operator" { + source = "./images/keycloak-operator" + target_repository = "${var.target_repository}/keycloak-operator" +} + module "ko" { source = "./images/ko" target_repository = "${var.target_repository}/ko" @@ -2011,6 +2016,10 @@ output "summary_keycloak" { value = module.keycloak.summary } +output "summary_keycloak-operator" { + value = module.keycloak-operator.summary +} + output "summary_ko" { value = module.ko.summary } diff --git a/images/keycloak-operator/README.md b/images/keycloak-operator/README.md new file mode 100644 index 0000000000..c9c3d5771a --- /dev/null +++ b/images/keycloak-operator/README.md @@ -0,0 +1,216 @@ + +# keycloak-operator +| | | +| - | - | +| **OCI Reference** | `cgr.dev/chainguard/keycloak-operator` | + + +* [View Image in Chainguard Academy](https://edu.chainguard.dev/chainguard/chainguard-images/reference/keycloak-operator/overview/) +* [View Image Catalog](https://console.enforce.dev/images/catalog) for a full list of available tags. +* [Contact Chainguard](https://www.chainguard.dev/chainguard-images) for enterprise support, SLAs, and access to older tags.* + +--- + + + +A Kubernetes Operator based on the Operator SDK for installing and managing Keycloak. + + + +## Download this Image +The image is available on `cgr.dev`: + +``` +docker pull cgr.dev/chainguard/keycloak-operator:latest +``` + + + +## Usage + +### Kubernetes + +You can install the Operator on a vanilla Kubernetes cluster by using kubectl commands: + +Install the CRDs by entering the following commands: + +```bash +kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/24.0.4/kubernetes/keycloaks.k8s.keycloak.org-v1.yml +kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/24.0.4/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml +``` + +Next, install the Keycloak operator with Chainguard images using following steps: + + +##### Step 1: Download the YAML file and save it with a different name +curl -o keycloak-operator.yml https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/24.0.4/kubernetes/kubernetes.yml + +##### Step 2: Use sed to replace the image repository for Keycloak (adjust for macOS) +sed -i '' 's|quay\.io/keycloak/keycloak:.*|cgr.dev/chainguard/keycloak:latest|' keycloak-operator.yml + +##### Step 3: Use sed to replace the image repository for Keycloak Operator (adjust for macOS) +sed -i '' 's|quay\.io/keycloak/keycloak-operator:.*|cgr.dev/chainguard/keycloak-operator:latest|' keycloak-operator.yml + +##### Step 4: Apply the modified YAML file +kubectl apply -f keycloak-operator.yml + +**NOTE** : The above sed commands were for MacOS (BSD based), for Linux GNU based, replace `sed -i '' 's` with `sed -i 's` + +Currently the Operator watches only the namespace where the Operator is installed. + + +### Basic Keycloak deployment with Keycloak Operator + +Once the Keycloak Operator is installed and running in the cluster namespace, you can set up the other deployment prerequisites. + +* Database + +* Hostname + +* TLS Certificate and associated keys + +#### Database + +For development purposes, you can use an ephemeral PostgreSQL pod installation. To provision it, follow the approach below: + +Create YAML file `example-postgres.yaml`: + +```yaml +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:15 + volumeMounts: + - mountPath: /data + name: cache-volume + env: + - name: POSTGRES_USER + value: testuser + - name: POSTGRES_PASSWORD + value: testpassword + - name: PGDATA + value: /data/pgdata + - name: POSTGRES_DB + value: keycloak + volumes: + - name: cache-volume + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres-db +spec: + selector: + app: postgresql-db + type: LoadBalancer + ports: + - port: 5432 + targetPort: 5432 +``` + +Apply the changes: + +```bash +kubectl apply -f example-postgres.yaml +``` + +#### Hostname + +For a production ready installation, you need a hostname that can be used to contact Keycloak. See [Configuring the hostname](https://www.keycloak.org/server/hostname) for the available configurations. + +For development purposes, this guide will use `test.keycloak.org`. + +#### TLS Certificate and key +See your Certification Authority to obtain the certificate and the key. + +For development purposes, you can enter this command to obtain a self-signed certificate: + +```bash +openssl req -subj '/CN=test.keycloak.org/O=Test Keycloak./C=US' -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem +``` + +You should install it in the cluster namespace as a Secret by entering this command: + +```bash +kubectl create secret tls example-tls-secret --cert certificate.pem --key key.pem +``` + +### Deploying Keycloak + +Consider storing the Database credentials in a separate Secret. Enter the following commands: + +```bash +kubectl create secret generic keycloak-db-secret \ + --from-literal=username=testuser \ + --from-literal=password=testpassword +``` + +For a basic deployment, you can stick to the following approach: + +Create YAML file `example-kc.yaml`: + +```yaml +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: example-kc +spec: + instances: 1 + db: + vendor: postgres + host: postgres-db + usernameSecret: + name: keycloak-db-secret + key: username + passwordSecret: + name: keycloak-db-secret + key: password + http: + tlsSecret: example-tls-secret + hostname: + hostname: test.keycloak.org + proxy: + headers: xforwarded # double check your reverse proxy sets and overwrites the X-Forwarded-* headers +``` + +Apply the changes: + +```bash +kubectl apply -f example-kc.yaml +``` +To check that the Keycloak instance has been provisioned in the cluster, check the status of the created CR by entering the following command: + +```bash +kubectl get keycloaks/example-kc -o go-template='{{range .status.conditions}}CONDITION: {{.type}}{{"\n"}} STATUS: {{.status}}{{"\n"}} MESSAGE: {{.message}}{{"\n"}}{{end}}' +``` +When the deployment is ready, look for output similar to the following: + +```yaml +CONDITION: Ready + STATUS: true + MESSAGE: +CONDITION: HasErrors + STATUS: false + MESSAGE: +CONDITION: RollingUpdate + STATUS: false + MESSAGE: +``` + +For further reference, please refer to [official documentation of the project](https://www.keycloak.org/guides#operator) + diff --git a/images/keycloak-operator/config/main.tf b/images/keycloak-operator/config/main.tf new file mode 100644 index 0000000000..bb26bd3b6f --- /dev/null +++ b/images/keycloak-operator/config/main.tf @@ -0,0 +1,22 @@ +terraform { + required_providers { + apko = { source = "chainguard-dev/apko" } + } +} + +variable "extra_packages" { + description = "The additional packages to install" + default = [ + "keycloak-operator", + "keycloak-operator-compat" + ] +} + +data "apko_config" "this" { + config_contents = file("${path.module}/template.apko.yaml") + extra_packages = var.extra_packages +} + +output "config" { + value = jsonencode(data.apko_config.this.config) +} \ No newline at end of file diff --git a/images/keycloak-operator/config/template.apko.yaml b/images/keycloak-operator/config/template.apko.yaml new file mode 100644 index 0000000000..070ad50b5c --- /dev/null +++ b/images/keycloak-operator/config/template.apko.yaml @@ -0,0 +1,25 @@ +contents: + packages: + +accounts: + groups: + - groupname: keycloak + gid: 1000 + users: + - username: keycloak + uid: 1000 + gid: 1000 + run-as: 1000 + +paths: + - path: /opt/keycloak + type: directory + permissions: 0o777 + uid: 1000 + gid: 1000 + recursive: true + +work-dir: /opt/keycloak + +entrypoint: + command: java -Djava.util.logging.manager=org.jboss.logmanager.LogManager -jar quarkus-run.jar diff --git a/images/keycloak-operator/generated.tf b/images/keycloak-operator/generated.tf new file mode 100644 index 0000000000..94e7c18f82 --- /dev/null +++ b/images/keycloak-operator/generated.tf @@ -0,0 +1,13 @@ +# DO NOT EDIT - this file is autogenerated by tfgen + +output "summary" { + value = merge( + { + basename(path.module) = { + "ref" = module.keycloak-operator.image_ref + "config" = module.keycloak-operator.config + "tags" = ["latest"] + } + }) +} + diff --git a/images/keycloak-operator/main.tf b/images/keycloak-operator/main.tf new file mode 100644 index 0000000000..566102e6d9 --- /dev/null +++ b/images/keycloak-operator/main.tf @@ -0,0 +1,39 @@ +terraform { + required_providers { + oci = { source = "chainguard-dev/oci" } + } +} + +variable "target_repository" { + description = "The docker repo into which the image and attestations should be published." +} + +module "config" { source = "./config" } + +module "keycloak-operator" { + source = "../../tflib/publisher" + name = basename(path.module) + target_repository = var.target_repository + config = module.config.config + + build-dev = true + +} + +module "test" { + source = "./tests" + digest = module.keycloak-operator.image_ref +} + +resource "oci_tag" "latest" { + depends_on = [module.test] + digest_ref = module.keycloak-operator.image_ref + tag = "latest" +} + +resource "oci_tag" "latest-dev" { + depends_on = [module.test] + digest_ref = module.keycloak-operator.dev_ref + tag = "latest-dev" +} + diff --git a/images/keycloak-operator/metadata.yaml b/images/keycloak-operator/metadata.yaml new file mode 100644 index 0000000000..1da50242e4 --- /dev/null +++ b/images/keycloak-operator/metadata.yaml @@ -0,0 +1,14 @@ +name: keycloak-operator +image: cgr.dev/chainguard/keycloak-operator +logo: https://storage.googleapis.com/chainguard-academy/logos/keycloak-operator.svg +endoflife: "" +console_summary: "" +short_description: "A Kubernetes Operator based on the Operator SDK for installing and managing Keycloak." +compatibility_notes: "" +readme_file: README.md +upstream_url: https://github.com/keycloak/keycloak/tree/main/operator +keywords: + - application + - tools + - operator + - kubernetes diff --git a/images/keycloak-operator/tests/keycloak-test.sh b/images/keycloak-operator/tests/keycloak-test.sh new file mode 100755 index 0000000000..4050af58a3 --- /dev/null +++ b/images/keycloak-operator/tests/keycloak-test.sh @@ -0,0 +1,515 @@ +#!/usr/bin/env bash + +set -o errexit -o nounset -o errtrace -o pipefail -x + +TMPDIR="$(mktemp -d)" +NAMESPACE="keycloak-test" +apk add openssl + +# Create Namespace and CRDs + +# Check if the namespace exists by searching for its name in the list of all namespaces +if kubectl get ns | grep -qw $NAMESPACE; then + echo "Namespace $NAMESPACE already exists." +else + echo "Namespace $NAMESPACE does not exist. Creating it now..." + kubectl create namespace $NAMESPACE + if [ $? -eq 0 ]; then + echo "Namespace $NAMESPACE created successfully." + else + echo "Failed to create namespace $NAMESPACE." + fi +fi + +kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/24.0.4/kubernetes/keycloaks.k8s.keycloak.org-v1.yml +kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/24.0.4/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml + +# Apply the keycloak-operator manifest +cat < "${TMPDIR}/minimal-keycloak-operator-manifest.yaml" +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + annotations: + app.quarkus.io/quarkus-version: 3.8.3 + app.quarkus.io/vcs-uri: https://github.com/keycloak/keycloak.git + app.quarkus.io/build-timestamp: 2024-05-07 - 12:29:12 +0000 + labels: + app.kubernetes.io/name: keycloak-operator + app.kubernetes.io/version: 24.0.3 + app.kubernetes.io/managed-by: quarkus + name: keycloak-operator +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: keycloak-operator-clusterrole +rules: + - apiGroups: + - config.openshift.io + resources: + - ingresses + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: keycloakrealmimportcontroller-cluster-role +rules: + - apiGroups: + - k8s.keycloak.org + resources: + - keycloakrealmimports + - keycloakrealmimports/status + - keycloakrealmimports/finalizers + verbs: + - get + - list + - watch + - patch + - update + - create + - delete + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - patch + - update + - delete + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: keycloakcontroller-cluster-role +rules: + - apiGroups: + - k8s.keycloak.org + resources: + - keycloaks + - keycloaks/status + - keycloaks/finalizers + verbs: + - get + - list + - watch + - patch + - update + - create + - delete + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - patch + - update + - delete + - create + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - patch + - update + - delete + - create + - apiGroups: + - "" + resources: + - services + verbs: + - get + - list + - watch + - patch + - update + - delete + - create + - apiGroups: + - networking.k8s.io + resources: + - ingresses + verbs: + - get + - list + - watch + - patch + - update + - delete + - create + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch + - delete + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: keycloak-operator + name: keycloak-operator-clusterrole-binding +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: keycloak-operator-clusterrole +subjects: + - kind: ServiceAccount + name: keycloak-operator + namespace: keycloak +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: keycloak-operator-role +rules: + - apiGroups: + - apps + resources: + - statefulsets + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - secrets + - services + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - "" + resources: + - pods + verbs: + - list + - apiGroups: + - batch + resources: + - jobs + verbs: + - get + - list + - watch + - create + - delete + - patch + - update + - apiGroups: + - networking.k8s.io + resources: + - ingresses + 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 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: keycloakrealmimportcontroller-role-binding +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: keycloakrealmimportcontroller-cluster-role +subjects: + - kind: ServiceAccount + name: keycloak-operator +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: keycloakcontroller-role-binding +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: keycloakcontroller-cluster-role +subjects: + - kind: ServiceAccount + name: keycloak-operator +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + labels: + app.kubernetes.io/name: keycloak-operator + app.kubernetes.io/version: 24.0.3 + name: keycloak-operator-view +roleRef: + kind: ClusterRole + apiGroup: rbac.authorization.k8s.io + name: view +subjects: + - kind: ServiceAccount + name: keycloak-operator +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + app.quarkus.io/quarkus-version: 3.8.3 + app.quarkus.io/vcs-uri: https://github.com/keycloak/keycloak.git + app.quarkus.io/build-timestamp: 2024-05-07 - 12:29:12 +0000 + labels: + app.kubernetes.io/name: keycloak-operator + app.kubernetes.io/version: 24.0.3 + app.kubernetes.io/managed-by: quarkus + name: keycloak-operator +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8080 + selector: + app.kubernetes.io/name: keycloak-operator + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + app.quarkus.io/quarkus-version: 3.8.3 + app.quarkus.io/vcs-uri: https://github.com/keycloak/keycloak.git + app.quarkus.io/build-timestamp: 2024-05-07 - 12:29:12 +0000 + labels: + app.kubernetes.io/name: keycloak-operator + app.kubernetes.io/version: 24.0.3 + app.kubernetes.io/managed-by: quarkus + name: keycloak-operator +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: keycloak-operator + template: + metadata: + annotations: + app.quarkus.io/quarkus-version: 3.8.3 + app.quarkus.io/vcs-uri: https://github.com/keycloak/keycloak.git + app.quarkus.io/build-timestamp: 2024-05-07 - 12:29:12 +0000 + labels: + app.kubernetes.io/managed-by: quarkus + app.kubernetes.io/version: 24.0.3 + app.kubernetes.io/name: keycloak-operator + spec: + containers: + - env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: RELATED_IMAGE_KEYCLOAK + value: cgr.dev/chainguard/keycloak:latest + image: ${IMAGE_NAME} + imagePullPolicy: Always + livenessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/live + port: 8080 + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 10 + name: keycloak-operator + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /q/health/ready + port: 8080 + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 10 + startupProbe: + failureThreshold: 3 + httpGet: + path: /q/health/started + port: 8080 + scheme: HTTP + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 10 + serviceAccountName: keycloak-operator +EOF +kubectl create -f "${TMPDIR}/minimal-keycloak-operator-manifest.yaml" -n ${NAMESPACE} + +sleep 15 + +kubectl wait --for=condition=ready pod --selector app.kubernetes.io/name=keycloak-operator --namespace ${NAMESPACE} --timeout=5m + +logs=$(kubectl logs -l app.kubernetes.io/name=keycloak-operator -n ${NAMESPACE}) + +sleep 10 + +echo "$logs" | grep "Listening on: http://0.0.0.0:8080" + +cat < "${TMPDIR}/example-postgres.yaml" +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: postgresql-db + namespace: ${NAMESPACE} +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:15 + volumeMounts: + - mountPath: /data + name: cache-volume + env: + - name: POSTGRES_USER + value: testuser + - name: POSTGRES_PASSWORD + value: testpassword + - name: PGDATA + value: /data/pgdata + - name: POSTGRES_DB + value: keycloak + volumes: + - name: cache-volume + emptyDir: {} +--- +apiVersion: v1 +kind: Service +metadata: + name: postgres-db + namespace: ${NAMESPACE} +spec: + selector: + app: postgresql-db + type: LoadBalancer + ports: + - port: 5432 + targetPort: 5432 +EOF + +kubectl apply -f "${TMPDIR}"/example-postgres.yaml -n ${NAMESPACE} + +sleep 15 + +kubectl wait --for=condition=ready pod --selector statefulset.kubernetes.io/pod-name=postgresql-db-0 -n ${NAMESPACE} --timeout=5m + +chmod -R 777 "${TMPDIR}" + +openssl req -subj '/CN=test.keycloak.org/O=Test Keycloak./C=US' -newkey rsa:2048 -nodes -keyout "${TMPDIR}/key.pem" -x509 -days 365 -out "${TMPDIR}/certificate.pem" +kubectl create secret tls example-tls-secret --cert "${TMPDIR}/certificate.pem" --key "${TMPDIR}/key.pem" -n ${NAMESPACE} + +kubectl create secret generic keycloak-db-secret -n ${NAMESPACE} \ + --from-literal=username=testuser \ + --from-literal=password=testpassword + + +cat < "${TMPDIR}/example-kc.yaml" +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: example-kc + namespace: ${NAMESPACE} +spec: + instances: 1 + db: + vendor: postgres + host: postgres-db + usernameSecret: + name: keycloak-db-secret + key: username + passwordSecret: + name: keycloak-db-secret + key: password + http: + tlsSecret: example-tls-secret + hostname: + hostname: test.keycloak.org + proxy: + headers: xforwarded # double check your reverse proxy sets and overwrites the X-Forwarded-* headers +EOF + +kubectl apply -f "${TMPDIR}"/example-kc.yaml -n ${NAMESPACE} + +kubectl get keycloaks/example-kc -o go-template='{{range .status.conditions}}CONDITION: {{.type}}{{"\n"}} STATUS: {{.status}}{{"\n"}} MESSAGE: {{.message}}{{"\n"}}{{end}}' -n ${NAMESPACE} + +sleep 10 + +kubectl wait --for=condition=ready pod --selector statefulset.kubernetes.io/pod-name=example-kc-0 -n ${NAMESPACE} --timeout=5m + +klogs=$(kubectl logs -l app=keycloak -n ${NAMESPACE}) + +sleep 10 + +echo "$klogs" | grep "Listening on: https://0.0.0.0:8443" \ No newline at end of file diff --git a/images/keycloak-operator/tests/main.tf b/images/keycloak-operator/tests/main.tf new file mode 100644 index 0000000000..093483fcfe --- /dev/null +++ b/images/keycloak-operator/tests/main.tf @@ -0,0 +1,47 @@ +terraform { + required_providers { + oci = { source = "chainguard-dev/oci" } + imagetest = { source = "chainguard-dev/imagetest" } + } +} + +variable "digest" { + description = "The image digest to run tests over." +} + +data "imagetest_inventory" "this" {} + +resource "imagetest_harness_k3s" "this" { + name = "keycloak-operator" + inventory = data.imagetest_inventory.this + + sandbox = { + mounts = [ + { + source = path.module + destination = "/tests" + } + ] + envs = { + "IMAGE_NAME" = var.digest + } + } +} + +resource "imagetest_feature" "basic" { + name = "Basic" + description = "Keycloak operator tests that deploys Keycloak" + harness = imagetest_harness_k3s.this + + steps = [{ + name = "Smoke test" + cmd = <