Create default ServiceMonitor with Operator

Closes #40406

Signed-off-by: Ryan Emerson <remerson@ibm.com>
This commit is contained in:
Ryan Emerson
2025-09-16 09:57:35 +01:00
committed by GitHub
parent 20f9306b78
commit 6e7a836c96
25 changed files with 1276 additions and 139 deletions

View File

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

View File

@@ -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:

View File

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

View File

@@ -56,6 +56,11 @@
<artifactId>kubernetes-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.fabric8</groupId>
<artifactId>openshift-model-monitoring</artifactId>
<scope>provided</scope>
</dependency>
<!-- Quarkus -->
<dependency>

View File

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

View File

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

View File

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

View File

@@ -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!"

View File

@@ -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}

View File

@@ -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"

View File

@@ -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<Keycloak> {

View File

@@ -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<String> readConfigurationValue(String key, Keycloak keycloakCR, Context<Keycloak> context) {
private static Optional<String> readConfigurationValue(String key, Keycloak keycloakCR, Context<Keycloak> 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<Keycloak> 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);
}
}

View File

@@ -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<ServiceMonitor, Keycloak> {
public static class ReconcilePrecondition implements Condition<Ingress, Keycloak> {
@Override
public boolean isMet(DependentResource<Ingress, Keycloak> dependentResource, Keycloak primary,
Context<Keycloak> 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<Keycloak> 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();
}
}

View File

@@ -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 <vmuzikar@redhat.com>
*/
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<String, String> 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<String, String> configuredOptions(Keycloak keycloak) {
Map<String, String> 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<KeycloakSpec> 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<Volume> 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<Boolean> fetchIsRecreateUpdate(StatefulSet statefulSet) {
var value = statefulSet.getMetadata().getAnnotations().get(Constants.KEYCLOAK_RECREATE_UPDATE_ANNOTATION);
return Optional.ofNullable(value).map(Boolean::parseBoolean);

View File

@@ -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;
}
}

View File

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

View File

@@ -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:

View File

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

View File

@@ -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;
}
}

View File

@@ -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<NetworkPolicyPeer> rules) {
assertNotNull(rules);
assertEquals(3, rules.size());

View File

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

View File

@@ -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 <vmuzikar@redhat.com>
*/

View File

@@ -18,4 +18,7 @@ spec:
hostname:
hostname: example.com
proxy:
headers: xforwarded # default nginx ingress sets x-forwarded
headers: xforwarded # default nginx ingress sets x-forwarded
additionalOptions:
- name: metrics-enabled
value: "true"

View File

@@ -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: {}

View File

@@ -145,4 +145,8 @@ spec:
podTemplate:
metadata:
labels:
my-label: "foo"
my-label: "foo"
serviceMonitor:
enabled: true
interval: 30s
scrapeTimeout: 10s