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