diff --git a/.github/workflows/operator-ci.yml b/.github/workflows/operator-ci.yml
index e0e83e1896d..14e55cc2028 100644
--- a/.github/workflows/operator-ci.yml
+++ b/.github/workflows/operator-ci.yml
@@ -151,6 +151,11 @@ jobs:
source: github
opm: 1.21.0
+ - name: Install OC
+ uses: redhat-actions/openshift-tools-installer@144527c7d98999f2652264c048c7a9bd103f8a82 # v1.13.1
+ with:
+ oc: 4
+
- name: Install Yq
run: sudo snap install yq
@@ -173,25 +178,15 @@ jobs:
REGISTRY=$(minikube ip):5000 ./scripts/olm-testing.sh ${GITHUB_SHA::6}
- name: Deploy an example Keycloak and wait for it to be ready
- working-directory: operator
+ working-directory: operator/scripts
run: |
- kubectl apply -f src/test/resources/example-postgres.yaml
- ./scripts/check-crds-installed.sh
- kubectl apply -f src/test/resources/example-db-secret.yaml
- kubectl apply -f src/test/resources/example-tls-secret.yaml
- kubectl apply -f src/test/resources/example-keycloak.yaml
- kubectl apply -f src/test/resources/example-realm.yaml
- # Wait for the CRs to be ready
- ./scripts/check-examples-installed.sh
+ ./check-crd-installed.sh keycloaks
+ ./check-crd-installed.sh keycloakrealmimports
+ ./deploy-examples.sh
- name: Single namespace cleanup
- working-directory: operator
- run: |
- kubectl delete -f src/test/resources/example-postgres.yaml
- kubectl delete -f src/test/resources/example-db-secret.yaml
- kubectl delete -f src/test/resources/example-tls-secret.yaml
- kubectl delete -f src/test/resources/example-keycloak.yaml
- kubectl delete -f src/test/resources/example-realm.yaml
+ working-directory: operator/scripts
+ run: ./undeploy-examples.sh
- name: Arrange OLM test installation for all namespaces
working-directory: operator
@@ -200,16 +195,40 @@ jobs:
kubectl patch operatorgroup og --type json --patch '[{"op":"remove","path":"/spec/targetNamespaces"}]'
- name: Deploy an example Keycloak in a different namespace and wait for it to be ready
- working-directory: operator
+ working-directory: operator/scripts
run: |
kubectl create ns keycloak
- kubectl apply -f src/test/resources/example-postgres.yaml -n keycloak
- kubectl apply -f src/test/resources/example-db-secret.yaml -n keycloak
- kubectl apply -f src/test/resources/example-tls-secret.yaml -n keycloak
- kubectl apply -f src/test/resources/example-keycloak.yaml -n keycloak
- kubectl apply -f src/test/resources/example-realm.yaml -n keycloak
- # Wait for the CRs to be ready
- ./scripts/check-examples-installed.sh keycloak
+ ./deploy-examples.sh keycloak
+ ./undeploy-examples.sh keycloak
+
+ - name: Install ServiceMonitor CRD
+ working-directory: operator
+ run: |
+ kubectl apply -f src/test/resources/service-monitor-crds.yml
+ ./scripts/check-crd-installed.sh servicemonitors
+
+ - name: Deploy an example Keycloak with ServiceMonitor
+ working-directory: operator/scripts
+ run: |
+ ./deploy-examples.sh keycloak
+ kubectl -n keycloak wait servicemonitor/example-kc --for=jsonpath='{.metadata.name}' --timeout=60s
+
+ - name: Debug Custom Resources
+ if: failure()
+ run: |
+ kubectl get keycloaks -A -o yaml
+ kubectl get keycloakrealmimports -A -o yaml
+
+ - name: Gather inspect report
+ if: failure()
+ run: oc adm inspect ns
+
+ - name: Upload inspect report
+ if: failure()
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: oc-inspect
+ path: inspect.*
check:
name: Status Check - Keycloak Operator CI
diff --git a/docs/guides/observability/exemplars.adoc b/docs/guides/observability/exemplars.adoc
index 6e700988e0c..6b28bab8a5c 100644
--- a/docs/guides/observability/exemplars.adoc
+++ b/docs/guides/observability/exemplars.adoc
@@ -45,12 +45,12 @@ For Prometheus, this is a https://prometheus.io/docs/prometheus/latest/feature_f
. Scrape the metrics using the `OpenMetricsText1.0.0` protocol, which is not enabled by default in Prometheus.
+
-If you are using `PodMonitors` or similar in a Kubernetes environment, this can be achieved by adding it to the spec of the custom resource:
+If you are using a `ServiceMonitor` or similar in a Kubernetes environment, this can be achieved by adding it to the spec of the custom resource:
+
[source]
----
apiVersion: monitoring.coreos.com/v1
-kind: PodMonitor
+kind: ServiceMonitor
metadata:
...
spec:
diff --git a/docs/guides/operator/advanced-configuration.adoc b/docs/guides/operator/advanced-configuration.adoc
index f71c8240c4d..2ab72052c1a 100644
--- a/docs/guides/operator/advanced-configuration.adoc
+++ b/docs/guides/operator/advanced-configuration.adoc
@@ -100,7 +100,7 @@ spec:
----
NOTE: The name format of options defined in this way is identical to the key format of options specified in the configuration file.
For details on various configuration formats, see <@links.server id="configuration"/>.
-
+
==== Custom environment variables
You may find a need to set custom environment variables - such as for theme properties or `kc.[sh|bat]` script variables.
@@ -108,8 +108,8 @@ The `spec.env` field of the Keycloak CR allows you to directly set any environme
Logic in the operator based upon looking for value of a particular setting does not consult `spec.env`,
therefore do not use `spec.env` for anything that has a first-class configuration in the CR or may be specified as an `additionalOption`.
-Here's an example setting JAVA_OPTS_APPEND:
-
+Here's an example setting JAVA_OPTS_APPEND:
+
[source,yaml]
----
apiVersion: k8s.keycloak.org/v2alpha1
@@ -124,7 +124,7 @@ spec:
----
Similar to `additionalOptions` you may specify either a value or reference a Secret.
-
+
=== Secret References
Secret References are used by some dedicated options in the Keycloak CR, such as a `tlsSecret`, or as a value in `additionalOptions`.
@@ -526,4 +526,78 @@ spec:
annotation2: annotation-value2
----
+=== ServiceMonitor
+A `ServiceMonitor` resource is used to define how a service's metrics are discovered and scraped by Prometheus.
+The {project_name} Operator automatically generates a `ServiceMonitor` resource for your deployment. In order for the
+`ServiceMonitor` resource to be created, the `monitoring.coreos.com/v1:ServiceMonitor` Custom Resource Definition (CRD)
+must be installed on your {kubernetes} cluster.
+
+The Operator generates a `ServiceMonitor` with the following spec:
+
+.Default ServiceMonitor
+[source,yaml]
+----
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+ name: example-kc # <1>
+ namespace: example-namespace # <2>
+spec:
+ endpoints:
+ - interval: 30s
+ path: /metrics # <3>
+ port: management
+ scheme: https # <4>
+ scrapeTimeout: 10s
+ tlsConfig:
+ insecureSkipVerify: true
+ namespaceSelector:
+ matchNames:
+ - example-namespace # <2>
+ selector:
+ matchLabels:
+ app: keycloak
+ app.kubernetes.io/instance: example-kc # <2>
+ app.kubernetes.io/managed-by: keycloak-operator
+ scrapeProtocols:
+ - OpenMetricsText1.0.0
+----
+<1> The `ServiceMonitor` is created with the same name as the Keycloak CR.
+<2> The `ServiceMonitor` is always deployed to the same namespace as the Keycloak CR and will only match a Service
+in that namespace.
+<3> The configured path defaults to `/metrics`, but respects the `http-management-relative-path` value if configured.
+<4> The configured scheme is changed automatically depending on whether TLS is enabled or not.
+
+
+==== Modify ServiceMonitor configuration
+You can configure the `interval` and `scrapeTimeout` fields of the generated `ServiceMonitor` by modifying the Keycloak CR spec.
+
+.Modified ServiceMonitor
+[source,yaml]
+----
+apiVersion: k8s.keycloak.org/v2alpha1
+kind: Keycloak
+metadata:
+ name: example-kc
+spec:
+ serviceMonitor:
+ interval: 10s
+ scrapeTimeout: 5s
+----
+
+==== Disable ServiceMonitor creation
+You can prevent the Operator from creating a ServiceMonitor by configuring the Keycloak CR as follows:
+
+.Disabled ServiceMonitor
+[source,yaml]
+----
+apiVersion: k8s.keycloak.org/v2alpha1
+kind: Keycloak
+metadata:
+ name: example-kc
+spec:
+ serviceMonitor:
+ enabled: false
+----
+
@tmpl.guide>
diff --git a/operator/pom.xml b/operator/pom.xml
index 62339eb00cf..a0dd4cfbc20 100644
--- a/operator/pom.xml
+++ b/operator/pom.xml
@@ -56,6 +56,11 @@
kubernetes-junit-jupiter
test
+
+ io.fabric8
+ openshift-model-monitoring
+ provided
+
diff --git a/operator/scripts/Dockerfile-custom-image b/operator/scripts/Dockerfile-custom-image
index 86cd8647241..d7a74f9cf76 100644
--- a/operator/scripts/Dockerfile-custom-image
+++ b/operator/scripts/Dockerfile-custom-image
@@ -2,4 +2,4 @@ ARG IMAGE=keycloak
ARG VERSION=latest
FROM $IMAGE:$VERSION
-RUN /opt/keycloak/bin/kc.sh build --db=postgres --health-enabled=true --features=rolling-updates
+RUN /opt/keycloak/bin/kc.sh build --db=postgres --health-enabled=true --metrics-enabled=true --features=rolling-updates
diff --git a/operator/scripts/check-crd-installed.sh b/operator/scripts/check-crd-installed.sh
new file mode 100755
index 00000000000..93db89a2237
--- /dev/null
+++ b/operator/scripts/check-crd-installed.sh
@@ -0,0 +1,12 @@
+#! /bin/bash
+set -euo pipefail
+
+CRD=$1
+max_retries=240
+c=0
+while ! kubectl get "${CRD}"
+do
+ echo "$(date +"%T") Waiting for ${CRD} CRD"
+ ((c++)) && ((c==max_retries)) && exit -1
+ sleep 1
+done
diff --git a/operator/scripts/check-crds-installed.sh b/operator/scripts/check-crds-installed.sh
deleted file mode 100755
index 3c5448fabcf..00000000000
--- a/operator/scripts/check-crds-installed.sh
+++ /dev/null
@@ -1,19 +0,0 @@
-#! /bin/bash
-set -euo pipefail
-
-max_retries=240
-c=0
-while ! kubectl get keycloaks
-do
- echo "$(date +"%T") Waiting for Keycloak CRD"
- ((c++)) && ((c==max_retries)) && exit -1
- sleep 1
-done
-
-c=0
-while ! kubectl get keycloakrealmimports
-do
- echo "$(date +"%T") Waiting for Keycloak Realm Import CRD"
- ((c++)) && ((c==max_retries)) && exit -1
- sleep 1
-done
diff --git a/operator/scripts/create-olm-bundle.sh b/operator/scripts/create-olm-bundle.sh
index 7e395227591..30c305a17d2 100755
--- a/operator/scripts/create-olm-bundle.sh
+++ b/operator/scripts/create-olm-bundle.sh
@@ -69,6 +69,9 @@ yq ea -i '.spec.install.spec.deployments[0].spec.template.metadata.labels.name =
yq ea -i '.spec.install.spec.deployments[0].spec.template.spec.containers[0].env += [{"name": "POD_NAME", "valueFrom": {"fieldRef": {"fieldPath": "metadata.name"}}}]' "$CSV_PATH"
yq ea -i '.spec.install.spec.deployments[0].spec.template.spec.containers[0].env += [{"name": "OPERATOR_NAME", "value": "keycloak-operator"}]' "$CSV_PATH"
+# Remove ServiceMonitors GVK from nativeAPIS to allow CSV installation when CRDs not present
+yq ea -i 'del(.spec.nativeAPIs[] | select(.kind == "ServiceMonitor"))' "$CSV_PATH"
+
{ set +x; } 2>/dev/null
echo ""
echo "Created OLM bundle ok!"
diff --git a/operator/scripts/deploy-examples.sh b/operator/scripts/deploy-examples.sh
new file mode 100755
index 00000000000..9ff98256a43
--- /dev/null
+++ b/operator/scripts/deploy-examples.sh
@@ -0,0 +1,14 @@
+#! /bin/bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+NAMESPACE=${1:-default}
+
+kubectl apply -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-postgres.yaml"
+kubectl apply -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-db-secret.yaml"
+kubectl apply -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-tls-secret.yaml"
+kubectl apply -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-keycloak.yaml"
+kubectl apply -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-realm.yaml"
+
+# Wait for the CRs to be ready
+"${SCRIPT_DIR}"/check-examples-installed.sh ${NAMESPACE}
+
diff --git a/operator/scripts/undeploy-examples.sh b/operator/scripts/undeploy-examples.sh
new file mode 100755
index 00000000000..d9d1e71c4d0
--- /dev/null
+++ b/operator/scripts/undeploy-examples.sh
@@ -0,0 +1,9 @@
+#! /bin/bash
+SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
+
+NAMESPACE=${1:-default}
+kubectl delete -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-postgres.yaml"
+kubectl delete -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-db-secret.yaml"
+kubectl delete -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-tls-secret.yaml"
+kubectl delete -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-keycloak.yaml"
+kubectl delete -n "${NAMESPACE}" -f "${SCRIPT_DIR}/../src/test/resources/example-realm.yaml"
diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java
index 30546a66875..dcda20d8a26 100644
--- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java
+++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakController.java
@@ -34,6 +34,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.CRDPresentActivationCondition;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
@@ -63,7 +64,12 @@ import java.util.concurrent.TimeUnit;
@Dependent(type = KeycloakIngressDependentResource.class, reconcilePrecondition = KeycloakIngressDependentResource.EnabledCondition.class),
@Dependent(type = KeycloakServiceDependentResource.class),
@Dependent(type = KeycloakDiscoveryServiceDependentResource.class),
- @Dependent(type = KeycloakNetworkPolicyDependentResource.class, reconcilePrecondition = KeycloakNetworkPolicyDependentResource.EnabledCondition.class)
+ @Dependent(type = KeycloakNetworkPolicyDependentResource.class, reconcilePrecondition = KeycloakNetworkPolicyDependentResource.EnabledCondition.class),
+ @Dependent(
+ type = KeycloakServiceMonitorDependentResource.class,
+ activationCondition = CRDPresentActivationCondition.class,
+ reconcilePrecondition = KeycloakServiceMonitorDependentResource.ReconcilePrecondition.class
+ ),
})
public class KeycloakController implements Reconciler {
diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java
index 0b78d7b23b2..8dd455110f3 100644
--- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java
+++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java
@@ -356,36 +356,22 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
// Set bind address as this is required for JGroups to form a cluster in IPv6 environments
containerBuilder.addToArgs(0, "-Djgroups.bind.address=$(%s)".formatted(POD_IP));
- boolean tls = isTlsConfigured(keycloakCR);
- String protocol = tls ? "HTTPS" : "HTTP";
- int port = -1;
-
- if (readConfigurationValue(HTTP_MANAGEMENT_HEALTH_ENABLED, keycloakCR, context).map(Boolean::valueOf).orElse(true)) {
- port = HttpManagementSpec.managementPort(keycloakCR);
- if (readConfigurationValue(HTTP_MANAGEMENT_SCHEME, keycloakCR, context).filter("http"::equals).isPresent()) {
- protocol = "HTTP";
- }
- } else {
- port = tls ? HttpSpec.httpsPort(keycloakCR) : HttpSpec.httpPort(keycloakCR);
- }
+ var healthEnabled = readConfigurationValue(HTTP_MANAGEMENT_HEALTH_ENABLED, keycloakCR, context).map(Boolean::valueOf).orElse(true);
+ ManagementEndpoint endpoint = managementEndpoint(keycloakCR, context, healthEnabled);
// probes
var readinessOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getReadinessProbeSpec());
var livenessOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getLivenessProbeSpec());
var startupOptionalSpec = Optional.ofNullable(keycloakCR.getSpec().getStartupProbeSpec());
- var relativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_MANAGEMENT_RELATIVE_PATH_KEY, keycloakCR, context)
- .or(() -> readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY, keycloakCR, context))
- .map(path -> !path.endsWith("/") ? path + "/" : path)
- .orElse("/");
if (!containerBuilder.hasReadinessProbe()) {
containerBuilder.withNewReadinessProbe()
.withPeriodSeconds(readinessOptionalSpec.map(ProbeSpec::getProbePeriodSeconds).orElse(10))
.withFailureThreshold(readinessOptionalSpec.map(ProbeSpec::getProbeFailureThreshold).orElse(3))
.withNewHttpGet()
- .withScheme(protocol)
- .withNewPort(port)
- .withPath(relativePath + "health/ready")
+ .withScheme(endpoint.protocol)
+ .withNewPort(endpoint.port)
+ .withPath(endpoint.relativePath + "health/ready")
.endHttpGet()
.endReadinessProbe();
}
@@ -394,9 +380,9 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
.withPeriodSeconds(livenessOptionalSpec.map(ProbeSpec::getProbePeriodSeconds).orElse(10))
.withFailureThreshold(livenessOptionalSpec.map(ProbeSpec::getProbeFailureThreshold).orElse(3))
.withNewHttpGet()
- .withScheme(protocol)
- .withNewPort(port)
- .withPath(relativePath + "health/live")
+ .withScheme(endpoint.protocol)
+ .withNewPort(endpoint.port)
+ .withPath(endpoint.relativePath + "health/live")
.endHttpGet()
.endLivenessProbe();
}
@@ -405,9 +391,9 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
.withPeriodSeconds(startupOptionalSpec.map(ProbeSpec::getProbePeriodSeconds).orElse(1))
.withFailureThreshold(startupOptionalSpec.map(ProbeSpec::getProbeFailureThreshold).orElse(600))
.withNewHttpGet()
- .withScheme(protocol)
- .withNewPort(port)
- .withPath(relativePath + "health/started")
+ .withScheme(endpoint.protocol)
+ .withNewPort(endpoint.port)
+ .withPath(endpoint.relativePath + "health/started")
.endHttpGet()
.endStartupProbe();
}
@@ -606,7 +592,7 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
return keycloak.getMetadata().getName();
}
- protected Optional readConfigurationValue(String key, Keycloak keycloakCR, Context context) {
+ private static Optional readConfigurationValue(String key, Keycloak keycloakCR, Context context) {
return Optional.ofNullable(keycloakCR.getSpec()).map(KeycloakSpec::getAdditionalOptions)
.flatMap(l -> l.stream().filter(sc -> sc.getName().equals(key)).findFirst().map(serverConfigValue -> {
if (serverConfigValue.getValue() != null) {
@@ -658,4 +644,27 @@ public class KeycloakDeploymentDependentResource extends CRUDKubernetesDependent
toUpdate.getMetadata().getAnnotations().put(Constants.KEYCLOAK_UPDATE_REVISION_ANNOTATION, revision);
}
+ record ManagementEndpoint(String relativePath, String protocol, int port) {}
+
+ static ManagementEndpoint managementEndpoint(Keycloak keycloakCR, Context context, boolean useMgmtProtocolPort) {
+ boolean tls = isTlsConfigured(keycloakCR);
+ String protocol = tls ? "HTTPS" : "HTTP";
+ int port;
+
+ if (useMgmtProtocolPort) {
+ port = HttpManagementSpec.managementPort(keycloakCR);
+ if (readConfigurationValue(HTTP_MANAGEMENT_SCHEME, keycloakCR, context).filter("http"::equals).isPresent()) {
+ protocol = "HTTP";
+ }
+ } else {
+ port = tls ? HttpSpec.httpsPort(keycloakCR) : HttpSpec.httpPort(keycloakCR);
+ }
+
+ var relativePath = readConfigurationValue(Constants.KEYCLOAK_HTTP_MANAGEMENT_RELATIVE_PATH_KEY, keycloakCR, context)
+ .or(() -> readConfigurationValue(Constants.KEYCLOAK_HTTP_RELATIVE_PATH_KEY, keycloakCR, context))
+ .map(path -> !path.endsWith("/") ? path + "/" : path)
+ .orElse("/");
+
+ return new ManagementEndpoint(relativePath, protocol, port);
+ }
}
diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceMonitorDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceMonitorDependentResource.java
new file mode 100644
index 00000000000..8d7f622ee4f
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakServiceMonitorDependentResource.java
@@ -0,0 +1,71 @@
+package org.keycloak.operator.controllers;
+
+import static org.keycloak.operator.controllers.KeycloakDeploymentDependentResource.managementEndpoint;
+import static org.keycloak.operator.crds.v2alpha1.CRDUtils.LEGACY_MANAGEMENT_ENABLED;
+import static org.keycloak.operator.crds.v2alpha1.CRDUtils.METRICS_ENABLED;
+import static org.keycloak.operator.crds.v2alpha1.CRDUtils.configuredOptions;
+
+import org.keycloak.operator.Constants;
+import org.keycloak.operator.Utils;
+import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
+import org.keycloak.operator.crds.v2alpha1.deployment.spec.ServiceMonitorSpec;
+
+import io.fabric8.kubernetes.api.model.networking.v1.Ingress;
+import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitor;
+import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitorBuilder;
+import io.javaoperatorsdk.operator.api.config.informer.Informer;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
+import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
+import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
+
+@KubernetesDependent(
+ informer = @Informer(labelSelector = Constants.DEFAULT_LABELS_AS_STRING)
+)
+public class KeycloakServiceMonitorDependentResource extends CRUDKubernetesDependentResource {
+
+ public static class ReconcilePrecondition implements Condition {
+ @Override
+ public boolean isMet(DependentResource dependentResource, Keycloak primary,
+ Context context) {
+ var opts = configuredOptions(primary);
+ if (Boolean.parseBoolean(opts.get(LEGACY_MANAGEMENT_ENABLED)) || !Boolean.parseBoolean(opts.getOrDefault(METRICS_ENABLED, "false")))
+ return false;
+
+ return ServiceMonitorSpec.get(primary).isEnabled();
+ }
+ }
+
+ @Override
+ protected ServiceMonitor desired(Keycloak primary, Context context) {
+ var endpoint = managementEndpoint(primary, context, true);
+ var meta = primary.getMetadata();
+ var spec = ServiceMonitorSpec.get(primary);
+ return new ServiceMonitorBuilder()
+ .withNewMetadata()
+ .withName(meta.getName())
+ .withNamespace(meta.getNamespace())
+ .endMetadata()
+ .withNewSpec()
+ .withNewNamespaceSelector()
+ .addToMatchNames(meta.getNamespace())
+ .endNamespaceSelector()
+ .withNewSelector()
+ .addToMatchLabels(Utils.allInstanceLabels(primary))
+ .endSelector()
+ .withScrapeProtocols("OpenMetricsText1.0.0")
+ .addNewEndpoint()
+ .withInterval(spec.getInterval())
+ .withPath(endpoint.relativePath() + "metrics")
+ .withPort(Constants.KEYCLOAK_MANAGEMENT_PORT_NAME)
+ .withScheme(endpoint.protocol().toLowerCase())
+ .withScrapeTimeout(spec.getScrapeTimeout())
+ .withNewTlsConfig()
+ .withInsecureSkipVerify(true)
+ .endTlsConfig()
+ .endEndpoint()
+ .endSpec()
+ .build();
+ }
+}
diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java
index 6dd7abcf2d9..6c94327e2d4 100644
--- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java
+++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/CRDUtils.java
@@ -26,28 +26,29 @@ import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
-import com.fasterxml.jackson.databind.JsonNode;
-import io.fabric8.kubernetes.api.model.Container;
-import io.fabric8.kubernetes.api.model.ObjectMeta;
-import io.fabric8.kubernetes.api.model.PodSpec;
-import io.fabric8.kubernetes.api.model.PodTemplateSpec;
-import io.fabric8.kubernetes.api.model.Volume;
-import io.fabric8.kubernetes.api.model.apps.StatefulSet;
-import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec;
-import io.javaoperatorsdk.operator.api.reconciler.Context;
import org.keycloak.operator.Constants;
import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpSpec;
+import com.fasterxml.jackson.databind.JsonNode;
+
+import io.fabric8.kubernetes.api.model.Container;
+import io.fabric8.kubernetes.api.model.ObjectMeta;
+import io.fabric8.kubernetes.api.model.PodSpec;
+import io.fabric8.kubernetes.api.model.PodTemplateSpec;
+import io.fabric8.kubernetes.api.model.apps.StatefulSet;
+import io.fabric8.kubernetes.api.model.apps.StatefulSetSpec;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+
/**
* @author Vaclav Muzikar
*/
public final class CRDUtils {
- private static final String METRICS_ENABLED = "metrics-enabled";
private static final String HEALTH_ENABLED = "health-enabled";
- private static final String LEGACY_MANAGEMENT_ENABLED = "legacy-observability-interface";
+ public static final String METRICS_ENABLED = "metrics-enabled";
+ public static final String LEGACY_MANAGEMENT_ENABLED = "legacy-observability-interface";
public static boolean isTlsConfigured(Keycloak keycloakCR) {
var tlsSecret = keycloakSpecOf(keycloakCR).map(KeycloakSpec::getHttpSpec).map(HttpSpec::getTlsSecret);
@@ -64,17 +65,7 @@ public final class CRDUtils {
}
public static boolean isManagementEndpointEnabled(Keycloak keycloak) {
- Map options = new HashMap<>();
- // add default options
- Constants.DEFAULT_DIST_CONFIG_LIST
- .forEach(valueOrSecret -> options.put(valueOrSecret.getName(), valueOrSecret.getValue()));
- // overwrite the configured ones
- keycloakSpecOf(keycloak)
- .map(KeycloakSpec::getAdditionalOptions)
- .stream()
- .flatMap(Collection::stream)
- .forEach(valueOrSecret -> options.put(valueOrSecret.getName(), valueOrSecret.getValue()));
-
+ var options = configuredOptions(keycloak);
// Legacy management enabled
if (Boolean.parseBoolean(options.get(LEGACY_MANAGEMENT_ENABLED))) {
return false;
@@ -87,6 +78,20 @@ public final class CRDUtils {
.anyMatch(Boolean::parseBoolean);
}
+ public static Map configuredOptions(Keycloak keycloak) {
+ Map options = new HashMap<>();
+ // add default options
+ Constants.DEFAULT_DIST_CONFIG_LIST
+ .forEach(valueOrSecret -> options.put(valueOrSecret.getName(), valueOrSecret.getValue()));
+ // overwrite the configured ones
+ keycloakSpecOf(keycloak)
+ .map(KeycloakSpec::getAdditionalOptions)
+ .stream()
+ .flatMap(Collection::stream)
+ .forEach(valueOrSecret -> options.put(valueOrSecret.getName(), valueOrSecret.getValue()));
+ return options;
+ }
+
public static Optional keycloakSpecOf(Keycloak keycloak) {
return Optional.ofNullable(keycloak)
.map(Keycloak::getSpec);
@@ -107,16 +112,6 @@ public final class CRDUtils {
return kubernetesSerialization.convertValue(value, JsonNode.class);
}
- public static Stream volumesFromStatefulSet(StatefulSet statefulSet) {
- return Optional.of(statefulSet)
- .map(StatefulSet::getSpec)
- .map(StatefulSetSpec::getTemplate)
- .map(PodTemplateSpec::getSpec)
- .map(PodSpec::getVolumes)
- .stream()
- .flatMap(Collection::stream);
- }
-
public static Optional fetchIsRecreateUpdate(StatefulSet statefulSet) {
var value = statefulSet.getMetadata().getAnnotations().get(Constants.KEYCLOAK_RECREATE_UPDATE_ANNOTATION);
return Optional.ofNullable(value).map(Boolean::parseBoolean);
diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java
index 8a1981aeeb4..b0c03aed205 100644
--- a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java
+++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/KeycloakSpec.java
@@ -33,6 +33,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.NetworkPolicySpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProbeSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProxySpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.SchedulingSpec;
+import org.keycloak.operator.crds.v2alpha1.deployment.spec.ServiceMonitorSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.TracingSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.Truststore;
@@ -157,6 +158,9 @@ public class KeycloakSpec {
@JsonPropertyDescription("Configuration for startup probe, by default it is 1 for periodSeconds and 600 for failureThreshold")
private ProbeSpec startupProbeSpec;
+ @JsonProperty("serviceMonitor")
+ @JsonPropertyDescription("Configuration related to the generated ServiceMonitor")
+ private ServiceMonitorSpec serviceMonitorSpec;
public HttpSpec getHttpSpec() {
return httpSpec;
@@ -374,4 +378,12 @@ public class KeycloakSpec {
this.importSpec = importSpec;
}
+
+ public ServiceMonitorSpec getServiceMonitorSpec() {
+ return serviceMonitorSpec;
+ }
+
+ public void setServiceMonitorSpec(ServiceMonitorSpec serviceMonitorSpec) {
+ this.serviceMonitorSpec = serviceMonitorSpec;
+ }
}
diff --git a/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/ServiceMonitorSpec.java b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/ServiceMonitorSpec.java
new file mode 100644
index 00000000000..76c20291e69
--- /dev/null
+++ b/operator/src/main/java/org/keycloak/operator/crds/v2alpha1/deployment/spec/ServiceMonitorSpec.java
@@ -0,0 +1,61 @@
+package org.keycloak.operator.crds.v2alpha1.deployment.spec;
+
+import org.keycloak.operator.crds.v2alpha1.CRDUtils;
+import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
+import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakSpec;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+import io.fabric8.generator.annotation.Default;
+import io.sundr.builder.annotations.Buildable;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@Buildable(editableEnabled = false, builderPackage = "io.fabric8.kubernetes.api.builder")
+public class ServiceMonitorSpec {
+
+ public static final String DEFAULT_INTERVAL = "30s";
+ public static final String DEFAULT_SCRAPE_TIMEOUT = "10s";
+
+ @JsonPropertyDescription("Enables or disables the creation of the ServiceMonitor.")
+ @Default("true")
+ private boolean enabled = true;
+
+ @JsonPropertyDescription("Interval at which metrics should be scraped")
+ @Default(DEFAULT_INTERVAL)
+ private String interval = DEFAULT_INTERVAL;
+
+ @JsonPropertyDescription("Timeout after which the scrape is ended")
+ @Default(DEFAULT_SCRAPE_TIMEOUT)
+ private String scrapeTimeout = DEFAULT_SCRAPE_TIMEOUT;
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getInterval() {
+ return interval;
+ }
+
+ public void setInterval(String interval) {
+ this.interval = interval;
+ }
+
+ public String getScrapeTimeout() {
+ return scrapeTimeout;
+ }
+
+ public void setScrapeTimeout(String scrapeTimeout) {
+ this.scrapeTimeout = scrapeTimeout;
+ }
+
+ public static ServiceMonitorSpec get(Keycloak keycloak) {
+ return CRDUtils.keycloakSpecOf(keycloak)
+ .map(KeycloakSpec::getServiceMonitorSpec)
+ .orElse(new ServiceMonitorSpec());
+ }
+}
diff --git a/operator/src/main/kubernetes/kubernetes.yml b/operator/src/main/kubernetes/kubernetes.yml
index 685e2c2f287..d5265c6e280 100644
--- a/operator/src/main/kubernetes/kubernetes.yml
+++ b/operator/src/main/kubernetes/kubernetes.yml
@@ -72,12 +72,29 @@ rules:
- delete
- patch
- update
+ - apiGroups:
+ - monitoring.coreos.com
+ resources:
+ - servicemonitors
+ verbs:
+ - create
+ - delete
+ - get
+ - list
+ - update
+ - watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: keycloak-operator-clusterrole
rules:
+ - apiGroups:
+ - apiextensions.k8s.io
+ resources:
+ - customresourcedefinitions
+ verbs:
+ - get
- apiGroups:
- config.openshift.io
resources:
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java
index f2532acc5b2..8e302f59bc5 100644
--- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/BaseOperatorTest.java
@@ -38,6 +38,7 @@ import io.fabric8.kubernetes.client.dsl.Loggable;
import io.fabric8.kubernetes.client.dsl.PodResource;
import io.fabric8.kubernetes.client.dsl.Resource;
import io.fabric8.kubernetes.client.utils.Serialization;
+import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitor;
import io.javaoperatorsdk.operator.Operator;
import io.javaoperatorsdk.operator.api.config.BaseConfigurationService;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
@@ -212,9 +213,11 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
public static void createCRDs(KubernetesClient client) throws FileNotFoundException {
K8sUtils.set(client, new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER + "keycloaks.k8s.keycloak.org-v1.yml"));
K8sUtils.set(client, new FileInputStream(TARGET_KUBERNETES_GENERATED_YML_FOLDER + "keycloakrealmimports.k8s.keycloak.org-v1.yml"));
+ K8sUtils.set(client, BaseOperatorTest.class.getResourceAsStream("/service-monitor-crds.yml"));
Awaitility.await().pollInterval(100, TimeUnit.MILLISECONDS).untilAsserted(() -> client.resources(Keycloak.class).list());
Awaitility.await().pollInterval(100, TimeUnit.MILLISECONDS).untilAsserted(() -> client.resources(KeycloakRealmImport.class).list());
+ Awaitility.await().pollInterval(100, TimeUnit.MILLISECONDS).untilAsserted(() -> client.resources(ServiceMonitor.class).list());
}
private static void createOperator() {
@@ -540,18 +543,22 @@ public class BaseOperatorTest implements QuarkusTestAfterEachCallback {
* @return
*/
public static Keycloak getTestKeycloakDeployment(boolean disableProbes) {
- Keycloak kc = K8sUtils.getDefaultKeycloakDeployment();
- kc.getMetadata().setNamespace(getCurrentNamespace());
- String image = getTestCustomImage();
- if (image != null) {
- kc.getSpec().setImage(image);
- }
- if (disableProbes) {
- return disableProbes(kc);
- }
- return kc;
+ return getTestKeycloakDeployment(disableProbes, true);
}
+ public static Keycloak getTestKeycloakDeployment(boolean disableProbes, boolean setCustomImage) {
+ Keycloak kc = K8sUtils.getDefaultKeycloakDeployment();
+ kc.getMetadata().setNamespace(getCurrentNamespace());
+ String image = getTestCustomImage();
+ if (setCustomImage && image != null) {
+ kc.getSpec().setImage(image);
+ }
+ if (disableProbes) {
+ return disableProbes(kc);
+ }
+ return kc;
+ }
+
public static Keycloak disableProbes(Keycloak keycloak) {
KeycloakSpecBuilder specBuilder = new KeycloakSpecBuilder(keycloak.getSpec());
var podTemplateSpecBuilder = specBuilder.editOrNewUnsupported().editOrNewPodTemplate().editOrNewSpec();
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/ServiceMonitorTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/ServiceMonitorTest.java
new file mode 100644
index 00000000000..9ed6f5dec70
--- /dev/null
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/ServiceMonitorTest.java
@@ -0,0 +1,110 @@
+package org.keycloak.operator.testsuite.integration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.List;
+
+import org.awaitility.Awaitility;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
+import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret;
+import org.keycloak.operator.crds.v2alpha1.deployment.spec.ServiceMonitorSpecBuilder;
+import org.keycloak.operator.testsuite.utils.K8sUtils;
+
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.openshift.api.model.monitoring.v1.ServiceMonitor;
+import io.quarkus.test.junit.QuarkusTest;
+
+@Tag(BaseOperatorTest.SLOW)
+@QuarkusTest
+public class ServiceMonitorTest extends BaseOperatorTest {
+
+ @Test
+ public void testServiceMonitorDisabledNoMetrics() {
+ Assumptions.assumeTrue(isServiceMonitorAvailable(k8sclient));
+ var kc = getTestKeycloakDeployment(true, false);;
+ kc.getSpec().setAdditionalOptions(List.of(new ValueOrSecret("metrics-enabled", "false")));
+ K8sUtils.deployKeycloak(k8sclient, kc, true);
+
+ ServiceMonitor sm = getServiceMonitor(kc);
+ assertThat(sm).isNull();
+ }
+
+ @Test
+ public void testServiceMonitorCreatedWithMetricsEnabled() {
+ Assumptions.assumeTrue(isServiceMonitorAvailable(k8sclient));
+ var kc = getTestKeycloakDeployment(true, false);;
+ K8sUtils.deployKeycloak(k8sclient, kc, true);
+
+ Awaitility.await().untilAsserted(() -> {
+ var sm = getServiceMonitor(kc);
+ assertThat(sm).isNotNull();
+ assertThat(sm.getSpec().getEndpoints()).hasSize(1);
+ });
+ }
+
+ @Test
+ public void testServiceMonitorDisabledExplicitly() {
+ Assumptions.assumeTrue(isServiceMonitorAvailable(k8sclient));
+ var kc = getTestKeycloakDeployment(true, false);;
+ kc.getSpec().setServiceMonitorSpec(
+ new ServiceMonitorSpecBuilder()
+ .withEnabled(false)
+ .build()
+ );
+ K8sUtils.deployKeycloak(k8sclient, kc, true);
+
+ ServiceMonitor sm = getServiceMonitor(kc);
+ assertThat(sm).isNull();
+ }
+
+ @Test
+ public void testServiceMonitorDisabledLegacyManagement() {
+ Assumptions.assumeTrue(isServiceMonitorAvailable(k8sclient));
+ var kc = getTestKeycloakDeployment(true, false);;
+ kc.getSpec().setAdditionalOptions(List.of(new ValueOrSecret("legacy-observability-interface", "true")));
+ K8sUtils.deployKeycloak(k8sclient, kc, true);
+
+ ServiceMonitor sm = getServiceMonitor(kc);
+ assertThat(sm).isNull();
+ }
+
+ @Test
+ public void testServiceMonitorConfigProperties() {
+ Assumptions.assumeTrue(isServiceMonitorAvailable(k8sclient));
+ var kc = getTestKeycloakDeployment(true, false);;
+ kc.getSpec().setServiceMonitorSpec(
+ new ServiceMonitorSpecBuilder()
+ .withInterval("1s")
+ .withScrapeTimeout("2s")
+ .build()
+ );
+ K8sUtils.deployKeycloak(k8sclient, kc, true);
+
+ Awaitility.await().untilAsserted(() -> {
+ var sm = getServiceMonitor(kc);
+ assertThat(sm).isNotNull();
+ assertThat(sm.getSpec().getEndpoints()).hasSize(1);
+ assertThat(sm.getSpec().getEndpoints().get(0).getInterval()).isEqualTo("1s");
+ assertThat(sm.getSpec().getEndpoints().get(0).getScrapeTimeout()).isEqualTo("2s");
+ });
+ }
+
+ private ServiceMonitor getServiceMonitor(Keycloak kc) {
+ return k8sclient.resources(ServiceMonitor.class)
+ .inNamespace(kc.getMetadata().getNamespace())
+ .withName(kc.getMetadata().getName())
+ .get();
+ }
+
+ private boolean isServiceMonitorAvailable(KubernetesClient client) {
+ return client
+ .apiextensions()
+ .v1()
+ .customResourceDefinitions()
+ .withName(new ServiceMonitor().getFullResourceName())
+ .get() != null;
+ }
+}
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java
index c409fe53b2a..bfdf2cfcaf9 100644
--- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/CRSerializationTest.java
@@ -33,6 +33,7 @@ import org.keycloak.operator.crds.v2alpha1.deployment.spec.DatabaseSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.FeatureSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpec;
+import org.keycloak.operator.crds.v2alpha1.deployment.spec.ServiceMonitorSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.TracingSpec;
import org.keycloak.operator.crds.v2alpha1.deployment.spec.TransactionsSpec;
import org.keycloak.operator.crds.v2alpha1.realmimport.KeycloakRealmImport;
@@ -292,6 +293,18 @@ public class CRSerializationTest {
assertEquals("1", revision);
}
+ @Test
+ public void serviceMonitorSpecification() {
+ Keycloak keycloak = Serialization.unmarshal(this.getClass().getResourceAsStream("/test-serialization-keycloak-cr.yml"), Keycloak.class);
+
+ ServiceMonitorSpec serviceMonitorSpec = keycloak.getSpec().getServiceMonitorSpec();
+ assertThat(serviceMonitorSpec, notNullValue());
+
+ assertThat(serviceMonitorSpec.isEnabled(), is(true));
+ assertThat(serviceMonitorSpec.getInterval(), is(ServiceMonitorSpec.DEFAULT_INTERVAL));
+ assertThat(serviceMonitorSpec.getScrapeTimeout(), is(ServiceMonitorSpec.DEFAULT_SCRAPE_TIMEOUT));
+ }
+
private static void assertNetworkPolicyRules(Collection rules) {
assertNotNull(rules);
assertEquals(3, rules.size());
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/unit/NetworkPolicyLogicTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/unit/NetworkPolicyLogicTest.java
index dba235cf8fd..02e83badfa4 100644
--- a/operator/src/test/java/org/keycloak/operator/testsuite/unit/NetworkPolicyLogicTest.java
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/unit/NetworkPolicyLogicTest.java
@@ -144,7 +144,8 @@ public class NetworkPolicyLogicTest {
namespaceSelectorWithMatchLabel("kubernetes.io/name", "keycloak")
));
var networkPolicy = assertEnabledAndGet(kc);
- CRAssert.assertIngressRules(networkPolicy, kc, -1, Constants.KEYCLOAK_HTTPS_PORT, -1);
+ var mgmtPort = legacyOption ? -1 : Constants.KEYCLOAK_MANAGEMENT_PORT;
+ CRAssert.assertIngressRules(networkPolicy, kc, -1, Constants.KEYCLOAK_HTTPS_PORT, mgmtPort);
}
@Test
diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java b/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java
index a32e77a764a..56bdf838a50 100644
--- a/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java
+++ b/operator/src/test/java/org/keycloak/operator/testsuite/utils/K8sUtils.java
@@ -17,23 +17,6 @@
package org.keycloak.operator.testsuite.utils;
-import io.fabric8.kubernetes.api.model.HasMetadata;
-import io.fabric8.kubernetes.api.model.PodBuilder;
-import io.fabric8.kubernetes.api.model.Secret;
-import io.fabric8.kubernetes.client.KubernetesClient;
-import io.fabric8.kubernetes.client.KubernetesClientException;
-import io.fabric8.kubernetes.client.dsl.ExecWatch;
-import io.fabric8.kubernetes.client.dsl.Resource;
-import io.fabric8.kubernetes.client.utils.Serialization;
-import io.quarkus.logging.Log;
-
-import org.awaitility.Awaitility;
-import org.keycloak.operator.Constants;
-import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
-import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
-import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpecBuilder;
-import org.keycloak.operator.crds.v2alpha1.deployment.spec.NetworkPolicySpecBuilder;
-
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
@@ -48,6 +31,23 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.awaitility.Awaitility;
+import org.keycloak.operator.Constants;
+import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak;
+import org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition;
+import org.keycloak.operator.crds.v2alpha1.deployment.spec.HttpManagementSpecBuilder;
+import org.keycloak.operator.crds.v2alpha1.deployment.spec.NetworkPolicySpecBuilder;
+
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.fabric8.kubernetes.api.model.PodBuilder;
+import io.fabric8.kubernetes.api.model.Secret;
+import io.fabric8.kubernetes.client.KubernetesClient;
+import io.fabric8.kubernetes.client.KubernetesClientException;
+import io.fabric8.kubernetes.client.dsl.ExecWatch;
+import io.fabric8.kubernetes.client.dsl.Resource;
+import io.fabric8.kubernetes.client.utils.Serialization;
+import io.quarkus.logging.Log;
+
/**
* @author Vaclav Muzikar
*/
diff --git a/operator/src/test/resources/example-keycloak.yaml b/operator/src/test/resources/example-keycloak.yaml
index d20ecc07b38..4a85a74e523 100644
--- a/operator/src/test/resources/example-keycloak.yaml
+++ b/operator/src/test/resources/example-keycloak.yaml
@@ -18,4 +18,7 @@ spec:
hostname:
hostname: example.com
proxy:
- headers: xforwarded # default nginx ingress sets x-forwarded
\ No newline at end of file
+ headers: xforwarded # default nginx ingress sets x-forwarded
+ additionalOptions:
+ - name: metrics-enabled
+ value: "true"
\ No newline at end of file
diff --git a/operator/src/test/resources/service-monitor-crds.yml b/operator/src/test/resources/service-monitor-crds.yml
new file mode 100644
index 00000000000..d8268850337
--- /dev/null
+++ b/operator/src/test/resources/service-monitor-crds.yml
@@ -0,0 +1,711 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.18.0
+ operator.prometheus.io/version: 0.85.0
+ name: servicemonitors.monitoring.coreos.com
+spec:
+ group: monitoring.coreos.com
+ names:
+ categories:
+ - prometheus-operator
+ kind: ServiceMonitor
+ listKind: ServiceMonitorList
+ plural: servicemonitors
+ shortNames:
+ - smon
+ singular: servicemonitor
+ scope: Namespaced
+ versions:
+ - name: v1
+ schema:
+ openAPIV3Schema:
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ properties:
+ attachMetadata:
+ properties:
+ node:
+ type: boolean
+ type: object
+ bodySizeLimit:
+ pattern: (^0|([0-9]*[.])?[0-9]+((K|M|G|T|E|P)i?)?B)$
+ type: string
+ convertClassicHistogramsToNHCB:
+ type: boolean
+ endpoints:
+ items:
+ properties:
+ authorization:
+ properties:
+ credentials:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type:
+ type: string
+ type: object
+ basicAuth:
+ properties:
+ password:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ username:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ bearerTokenFile:
+ type: string
+ bearerTokenSecret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ enableHttp2:
+ type: boolean
+ filterRunning:
+ type: boolean
+ followRedirects:
+ type: boolean
+ honorLabels:
+ type: boolean
+ honorTimestamps:
+ type: boolean
+ interval:
+ pattern: ^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$
+ type: string
+ metricRelabelings:
+ items:
+ properties:
+ action:
+ default: replace
+ enum:
+ - replace
+ - Replace
+ - keep
+ - Keep
+ - drop
+ - Drop
+ - hashmod
+ - HashMod
+ - labelmap
+ - LabelMap
+ - labeldrop
+ - LabelDrop
+ - labelkeep
+ - LabelKeep
+ - lowercase
+ - Lowercase
+ - uppercase
+ - Uppercase
+ - keepequal
+ - KeepEqual
+ - dropequal
+ - DropEqual
+ type: string
+ modulus:
+ format: int64
+ type: integer
+ regex:
+ type: string
+ replacement:
+ type: string
+ separator:
+ type: string
+ sourceLabels:
+ items:
+ pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$
+ type: string
+ type: array
+ targetLabel:
+ type: string
+ type: object
+ type: array
+ noProxy:
+ type: string
+ oauth2:
+ properties:
+ clientId:
+ properties:
+ configMap:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ secret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ clientSecret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ endpointParams:
+ additionalProperties:
+ type: string
+ type: object
+ noProxy:
+ type: string
+ proxyConnectHeader:
+ additionalProperties:
+ items:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
+ type: object
+ x-kubernetes-map-type: atomic
+ proxyFromEnvironment:
+ type: boolean
+ proxyUrl:
+ pattern: ^(http|https|socks5)://.+$
+ type: string
+ scopes:
+ items:
+ type: string
+ type: array
+ tlsConfig:
+ properties:
+ ca:
+ properties:
+ configMap:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ secret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ cert:
+ properties:
+ configMap:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ secret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ insecureSkipVerify:
+ type: boolean
+ keySecret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ maxVersion:
+ enum:
+ - TLS10
+ - TLS11
+ - TLS12
+ - TLS13
+ type: string
+ minVersion:
+ enum:
+ - TLS10
+ - TLS11
+ - TLS12
+ - TLS13
+ type: string
+ serverName:
+ type: string
+ type: object
+ tokenUrl:
+ minLength: 1
+ type: string
+ required:
+ - clientId
+ - clientSecret
+ - tokenUrl
+ type: object
+ params:
+ additionalProperties:
+ items:
+ type: string
+ type: array
+ type: object
+ path:
+ type: string
+ port:
+ type: string
+ proxyConnectHeader:
+ additionalProperties:
+ items:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: array
+ type: object
+ x-kubernetes-map-type: atomic
+ proxyFromEnvironment:
+ type: boolean
+ proxyUrl:
+ pattern: ^(http|https|socks5)://.+$
+ type: string
+ relabelings:
+ items:
+ properties:
+ action:
+ default: replace
+ enum:
+ - replace
+ - Replace
+ - keep
+ - Keep
+ - drop
+ - Drop
+ - hashmod
+ - HashMod
+ - labelmap
+ - LabelMap
+ - labeldrop
+ - LabelDrop
+ - labelkeep
+ - LabelKeep
+ - lowercase
+ - Lowercase
+ - uppercase
+ - Uppercase
+ - keepequal
+ - KeepEqual
+ - dropequal
+ - DropEqual
+ type: string
+ modulus:
+ format: int64
+ type: integer
+ regex:
+ type: string
+ replacement:
+ type: string
+ separator:
+ type: string
+ sourceLabels:
+ items:
+ pattern: ^[a-zA-Z_][a-zA-Z0-9_]*$
+ type: string
+ type: array
+ targetLabel:
+ type: string
+ type: object
+ type: array
+ scheme:
+ enum:
+ - http
+ - https
+ type: string
+ scrapeTimeout:
+ pattern: ^(0|(([0-9]+)y)?(([0-9]+)w)?(([0-9]+)d)?(([0-9]+)h)?(([0-9]+)m)?(([0-9]+)s)?(([0-9]+)ms)?)$
+ type: string
+ targetPort:
+ anyOf:
+ - type: integer
+ - type: string
+ x-kubernetes-int-or-string: true
+ tlsConfig:
+ properties:
+ ca:
+ properties:
+ configMap:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ secret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ caFile:
+ type: string
+ cert:
+ properties:
+ configMap:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ secret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ type: object
+ certFile:
+ type: string
+ insecureSkipVerify:
+ type: boolean
+ keyFile:
+ type: string
+ keySecret:
+ properties:
+ key:
+ type: string
+ name:
+ default: ""
+ type: string
+ optional:
+ type: boolean
+ required:
+ - key
+ type: object
+ x-kubernetes-map-type: atomic
+ maxVersion:
+ enum:
+ - TLS10
+ - TLS11
+ - TLS12
+ - TLS13
+ type: string
+ minVersion:
+ enum:
+ - TLS10
+ - TLS11
+ - TLS12
+ - TLS13
+ type: string
+ serverName:
+ type: string
+ type: object
+ trackTimestampsStaleness:
+ type: boolean
+ type: object
+ type: array
+ fallbackScrapeProtocol:
+ enum:
+ - PrometheusProto
+ - OpenMetricsText0.0.1
+ - OpenMetricsText1.0.0
+ - PrometheusText0.0.4
+ - PrometheusText1.0.0
+ type: string
+ jobLabel:
+ type: string
+ keepDroppedTargets:
+ format: int64
+ type: integer
+ labelLimit:
+ format: int64
+ type: integer
+ labelNameLengthLimit:
+ format: int64
+ type: integer
+ labelValueLengthLimit:
+ format: int64
+ type: integer
+ namespaceSelector:
+ properties:
+ any:
+ type: boolean
+ matchNames:
+ items:
+ type: string
+ type: array
+ type: object
+ nativeHistogramBucketLimit:
+ format: int64
+ type: integer
+ nativeHistogramMinBucketFactor:
+ anyOf:
+ - type: integer
+ - type: string
+ pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+ x-kubernetes-int-or-string: true
+ podTargetLabels:
+ items:
+ type: string
+ type: array
+ sampleLimit:
+ format: int64
+ type: integer
+ scrapeClass:
+ minLength: 1
+ type: string
+ scrapeClassicHistograms:
+ type: boolean
+ scrapeProtocols:
+ items:
+ enum:
+ - PrometheusProto
+ - OpenMetricsText0.0.1
+ - OpenMetricsText1.0.0
+ - PrometheusText0.0.4
+ - PrometheusText1.0.0
+ type: string
+ type: array
+ x-kubernetes-list-type: set
+ selector:
+ properties:
+ matchExpressions:
+ items:
+ properties:
+ key:
+ type: string
+ operator:
+ type: string
+ values:
+ items:
+ type: string
+ type: array
+ x-kubernetes-list-type: atomic
+ required:
+ - key
+ - operator
+ type: object
+ type: array
+ x-kubernetes-list-type: atomic
+ matchLabels:
+ additionalProperties:
+ type: string
+ type: object
+ type: object
+ x-kubernetes-map-type: atomic
+ selectorMechanism:
+ enum:
+ - RelabelConfig
+ - RoleSelector
+ type: string
+ targetLabels:
+ items:
+ type: string
+ type: array
+ targetLimit:
+ format: int64
+ type: integer
+ required:
+ - endpoints
+ - selector
+ type: object
+ status:
+ properties:
+ bindings:
+ items:
+ properties:
+ conditions:
+ items:
+ properties:
+ lastTransitionTime:
+ format: date-time
+ type: string
+ message:
+ type: string
+ observedGeneration:
+ format: int64
+ type: integer
+ reason:
+ type: string
+ status:
+ minLength: 1
+ type: string
+ type:
+ enum:
+ - Accepted
+ minLength: 1
+ type: string
+ required:
+ - lastTransitionTime
+ - status
+ - type
+ type: object
+ type: array
+ x-kubernetes-list-map-keys:
+ - type
+ x-kubernetes-list-type: map
+ group:
+ enum:
+ - monitoring.coreos.com
+ type: string
+ name:
+ minLength: 1
+ type: string
+ namespace:
+ minLength: 1
+ type: string
+ resource:
+ enum:
+ - prometheuses
+ - prometheusagents
+ type: string
+ required:
+ - group
+ - name
+ - namespace
+ - resource
+ type: object
+ type: array
+ type: object
+ required:
+ - spec
+ type: object
+ served: true
+ storage: true
+ subresources:
+ status: {}
diff --git a/operator/src/test/resources/test-serialization-keycloak-cr.yml b/operator/src/test/resources/test-serialization-keycloak-cr.yml
index 2e8cbc8841e..2028c87c09d 100644
--- a/operator/src/test/resources/test-serialization-keycloak-cr.yml
+++ b/operator/src/test/resources/test-serialization-keycloak-cr.yml
@@ -145,4 +145,8 @@ spec:
podTemplate:
metadata:
labels:
- my-label: "foo"
\ No newline at end of file
+ my-label: "foo"
+ serviceMonitor:
+ enabled: true
+ interval: 30s
+ scrapeTimeout: 10s