From 5707da1a1e21d4ab900aac7ce17846c7fdf6fad1 Mon Sep 17 00:00:00 2001 From: Aakash Dantre Date: Mon, 9 Nov 2026 17:00:25 +0000 Subject: [PATCH] feat(crdmanager): create ServiceMonitor and PodMonitor CRDs when missing Adds an opt-in controller that creates the monitoring.coreos.com/v1 ServiceMonitor and PodMonitor CRDs when they do not exist, so SM/PM scraping works without a manual CRD install. - Add pkg/crdmanager with the prometheus-operator v0.92.0 CRDs embedded, matching the client version in go.mod - Create a missing CRD with field manager amazon-cloudwatch-agent-operator, label cloudwatch.aws.amazon.com/crd-managed-by and annotations crd-applied-version and crd-applied-generation - Never update, patch or delete an existing CRD; evaluate an ownership check and log plus emit an Event once when the CRD is not the operator's - Treat a CRD as not the operator's when it carries another tool's markers (kubectl.kubernetes.io/last-applied-configuration, meta.helm.sh/release-name, app.kubernetes.io/managed-by) - Recreate a managed CRD after it is deleted, and treat AlreadyExists on create as success, so concurrent operator pods are safe - Watch CRD metadata only, filtered to the two names, and enqueue both names at start - Add --manage-prometheus-crds (default false) - Add RBAC for customresourcedefinitions get, list, watch and create, and regenerate config/rbac/role.yaml - Add unit tests and an envtest suite that runs the ownership check against a real API server --- config/rbac/role.yaml | 9 + main.go | 21 + pkg/crdmanager/assets.go | 38 + .../monitoring.coreos.com_podmonitors.yaml | 1402 ++++++++++++++++ ...monitoring.coreos.com_servicemonitors.yaml | 1419 +++++++++++++++++ pkg/crdmanager/crdmanager_suite_test.go | 52 + pkg/crdmanager/envtest_test.go | 359 +++++ pkg/crdmanager/reconciler.go | 293 ++++ pkg/crdmanager/reconciler_test.go | 371 +++++ 9 files changed, 3964 insertions(+) create mode 100644 pkg/crdmanager/assets.go create mode 100644 pkg/crdmanager/assets/monitoring.coreos.com_podmonitors.yaml create mode 100644 pkg/crdmanager/assets/monitoring.coreos.com_servicemonitors.yaml create mode 100644 pkg/crdmanager/crdmanager_suite_test.go create mode 100644 pkg/crdmanager/envtest_test.go create mode 100644 pkg/crdmanager/reconciler.go create mode 100644 pkg/crdmanager/reconciler_test.go diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 9587769ba..7f9920212 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -34,6 +34,15 @@ rules: - list - patch - watch +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - create + - get + - list + - watch - apiGroups: - apps resources: diff --git a/main.go b/main.go index 0f218fae2..719072890 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" "github.com/spf13/pflag" colfeaturegate "go.opentelemetry.io/collector/featuregate" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -37,6 +38,7 @@ import ( "github.com/aws/amazon-cloudwatch-agent-operator/internal/webhook/namespacemutation" "github.com/aws/amazon-cloudwatch-agent-operator/internal/webhook/podmutation" "github.com/aws/amazon-cloudwatch-agent-operator/internal/webhook/workloadmutation" + "github.com/aws/amazon-cloudwatch-agent-operator/pkg/crdmanager" "github.com/aws/amazon-cloudwatch-agent-operator/pkg/featuregate" "github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation" "github.com/aws/amazon-cloudwatch-agent-operator/pkg/instrumentation/auto" @@ -70,6 +72,7 @@ func init() { utilruntime.Must(otelv1alpha1.AddToScheme(scheme)) utilruntime.Must(routev1.AddToScheme(scheme)) utilruntime.Must(monitoringv1.AddToScheme(scheme)) + utilruntime.Must(apiextensionsv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } @@ -159,6 +162,7 @@ func main() { dcgmExporterImage string neuronMonitorImage string targetAllocatorImage string + managePrometheusCRDs bool ) pflag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") @@ -172,6 +176,7 @@ func main() { stringFlagOrEnv(&autoAnnotationConfigStr, "auto-annotation-config", "AUTO_ANNOTATION_CONFIG", "", "The configuration for auto-annotation.") pflag.StringVar(&autoMonitorConfigStr, "auto-monitor-config", "", "The configuration for auto-monitor.") pflag.StringVar(&autoInstrumentationConfigStr, "auto-instrumentation-config", "", "The configuration for auto-instrumentation.") + pflag.BoolVar(&managePrometheusCRDs, "manage-prometheus-crds", false, "Create the ServiceMonitor and PodMonitor CRDs (monitoring.coreos.com/v1) when they do not exist. The operator never deletes them and never changes a copy it did not create.") stringFlagOrEnv(&dcgmExporterImage, "dcgm-exporter-image", "RELATED_IMAGE_DCGM_EXPORTER", fmt.Sprintf("%s:%s", dcgmExporterImageRepository, v.DcgmExporter), "The default DCGM Exporter image. This image is used when no image is specified in the CustomResource.") stringFlagOrEnv(&neuronMonitorImage, "neuron-monitor-image", "RELATED_IMAGE_NEURON_MONITOR", fmt.Sprintf("%s:%s", neuronMonitorImageRepository, v.NeuronMonitor), "The default Neuron monitor image. This image is used when no image is specified in the CustomResource.") stringFlagOrEnv(&targetAllocatorImage, "target-allocator-image", "RELATED_IMAGE_TARGET_ALLOCATOR", fmt.Sprintf("%s:%s", targetAllocatorImageRepository, v.TargetAllocator), "The default AmazonCloudWatchAgent target allocator image. This image is used when no image is specified in the CustomResource.") @@ -309,6 +314,22 @@ func main() { os.Exit(1) } + if managePrometheusCRDs { + crdReconciler, crdErr := crdmanager.NewReconciler( + mgr.GetClient(), + ctrl.Log.WithName("controllers").WithName("PrometheusCRD"), + mgr.GetEventRecorderFor("amazon-cloudwatch-agent-operator"), //nolint:staticcheck // TODO: migrate to events.EventRecorder + ) + if crdErr == nil { + crdErr = crdReconciler.SetupWithManager(mgr) + } + if crdErr != nil { + setupLog.Error(crdErr, "unable to create controller", "controller", "PrometheusCRD") + os.Exit(1) + } + setupLog.Info("managing ServiceMonitor and PodMonitor CRDs", "version", crdmanager.BundledVersion) + } + decoder := admission.NewDecoder(mgr.GetScheme()) instrumentationAnnotator := auto.CreateInstrumentationAnnotator(autoMonitorConfigStr, autoAnnotationConfigStr, ctx, mgr.GetClient(), mgr.GetAPIReader(), setupLog) diff --git a/pkg/crdmanager/assets.go b/pkg/crdmanager/assets.go new file mode 100644 index 000000000..922bc0ef4 --- /dev/null +++ b/pkg/crdmanager/assets.go @@ -0,0 +1,38 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crdmanager + +import ( + _ "embed" + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/yaml" +) + +// BundledVersion is the prometheus-operator release the embedded ServiceMonitor +// and PodMonitor CRDs are pinned to. Keep this in sync with the asset files under +// assets/ and with the prometheus-operator API types vendored by the target +// allocator, so the schema the operator installs matches the schema the target +// allocator knows how to read. +const BundledVersion = "0.92.0" + +//go:embed assets/monitoring.coreos.com_servicemonitors.yaml +var serviceMonitorCRD []byte + +//go:embed assets/monitoring.coreos.com_podmonitors.yaml +var podMonitorCRD []byte + +// bundledCRDs decodes the embedded CRD manifests into typed objects. +func bundledCRDs() ([]*apiextensionsv1.CustomResourceDefinition, error) { + out := make([]*apiextensionsv1.CustomResourceDefinition, 0, 2) + for _, raw := range [][]byte{serviceMonitorCRD, podMonitorCRD} { + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := yaml.Unmarshal(raw, crd); err != nil { + return nil, fmt.Errorf("decode embedded CRD: %w", err) + } + out = append(out, crd) + } + return out, nil +} diff --git a/pkg/crdmanager/assets/monitoring.coreos.com_podmonitors.yaml b/pkg/crdmanager/assets/monitoring.coreos.com_podmonitors.yaml new file mode 100644 index 000000000..d7ebb3d74 --- /dev/null +++ b/pkg/crdmanager/assets/monitoring.coreos.com_podmonitors.yaml @@ -0,0 +1,1402 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + operator.prometheus.io/version: 0.92.0 + name: podmonitors.monitoring.coreos.com +spec: + group: monitoring.coreos.com + names: + categories: + - prometheus-operator + kind: PodMonitor + listKind: PodMonitorList + plural: podmonitors + shortNames: + - pmon + singular: podmonitor + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: |- + The `PodMonitor` custom resource definition (CRD) defines how `Prometheus` and `PrometheusAgent` can scrape metrics from a group of pods. + Among other things, it allows to specify: + * The pods to scrape via label selectors. + * The container ports to scrape. + * Authentication credentials to use. + * Target and metric relabeling. + + `Prometheus` and `PrometheusAgent` objects select `PodMonitor` objects using label and namespace selectors. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: spec defines the specification of desired Pod selection for + target discovery by Prometheus. + properties: + attachMetadata: + description: |- + attachMetadata defines additional metadata which is added to the + discovered targets. + + It requires Prometheus >= v2.35.0. + properties: + node: + description: |- + node when set to true, Prometheus attaches node metadata to the discovered + targets. + + The Prometheus service account must have the `list` and `watch` + permissions on the `Nodes` objects. + + Node metadata labels are not automatically added to scraped metrics. They are + exposed as `__meta_kubernetes_node_*` labels and can be copied to timeseries + with relabeling configuration. + type: boolean + type: object + bodySizeLimit: + description: |- + bodySizeLimit when defined specifies a job level limit on the size + of uncompressed response body that will be accepted by Prometheus. + + It requires Prometheus >= v2.28.0. + pattern: (^0|([0-9]*[.])?[0-9]+((K|M|G|T|E|P)i?)?B)$ + type: string + convertClassicHistogramsToNHCB: + description: |- + convertClassicHistogramsToNHCB defines whether to convert all scraped classic histograms into a native histogram with custom buckets. + It requires Prometheus >= v3.0.0. + type: boolean + fallbackScrapeProtocol: + description: |- + fallbackScrapeProtocol defines the protocol to use if a scrape returns blank, unparseable, or otherwise invalid Content-Type. + + It requires Prometheus >= v3.0.0. + enum: + - PrometheusProto + - OpenMetricsText0.0.1 + - OpenMetricsText1.0.0 + - PrometheusText0.0.4 + - PrometheusText1.0.0 + type: string + jobLabel: + description: |- + jobLabel defines the label to use to retrieve the job name from. + `jobLabel` selects the label from the associated Kubernetes `Pod` + object which will be used as the `job` label for all metrics. + + For example if `jobLabel` is set to `foo` and the Kubernetes `Pod` + object is labeled with `foo: bar`, then Prometheus adds the `job="bar"` + label to all ingested metrics. + + If the value of this field is empty, the `job` label of the metrics + defaults to the namespace and name of the PodMonitor object (e.g. `/`). + type: string + keepDroppedTargets: + description: |- + keepDroppedTargets defines the per-scrape limit on the number of targets dropped by relabeling + that will be kept in memory. 0 means no limit. + + It requires Prometheus >= v2.47.0. + format: int64 + type: integer + labelLimit: + description: |- + labelLimit defines the per-scrape limit on number of labels that will be accepted for a sample. + + It requires Prometheus >= v2.27.0. + format: int64 + type: integer + labelNameLengthLimit: + description: |- + labelNameLengthLimit defines the per-scrape limit on length of labels name that will be accepted for a sample. + + It requires Prometheus >= v2.27.0. + format: int64 + type: integer + labelValueLengthLimit: + description: |- + labelValueLengthLimit defines the per-scrape limit on length of labels value that will be accepted for a sample. + + It requires Prometheus >= v2.27.0. + format: int64 + type: integer + namespaceSelector: + description: |- + namespaceSelector defines in which namespace(s) Prometheus should discover the pods. + By default, the pods are discovered in the same namespace as the `PodMonitor` object but it is possible to select pods across different/all namespaces. + properties: + any: + description: |- + any defines the boolean describing whether all namespaces are selected in contrast to a + list restricting them. + type: boolean + matchNames: + description: matchNames defines the list of namespace names to + select from. + items: + type: string + type: array + type: object + nativeHistogramBucketLimit: + description: |- + nativeHistogramBucketLimit defines ff there are more than this many buckets in a native histogram, + buckets will be merged to stay within the limit. + It requires Prometheus >= v2.45.0. + format: int64 + type: integer + nativeHistogramMinBucketFactor: + anyOf: + - type: integer + - type: string + description: |- + nativeHistogramMinBucketFactor defines if the growth factor of one bucket to the next is smaller than this, + buckets will be merged to increase the factor sufficiently. + It requires Prometheus >= v2.50.0. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + podMetricsEndpoints: + description: podMetricsEndpoints defines how to scrape metrics from + the selected pods. + items: + description: |- + PodMetricsEndpoint defines an endpoint serving Prometheus metrics to be scraped by + Prometheus. + properties: + authorization: + description: |- + authorization configures the Authorization header credentials used by + the client. + + Cannot be set at the same time as `basicAuth`, `bearerTokenSecret` or `oauth2`. + properties: + credentials: + description: credentials defines a key of a Secret in the + namespace that contains the credentials for authentication. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: + description: |- + type defines the authentication type. The value is case-insensitive. + + "Basic" is not a supported value. + + Default: "Bearer" + type: string + type: object + basicAuth: + description: |- + basicAuth defines the Basic Authentication credentials used by the + client. + + Cannot be set at the same time as `authorization`, `bearerTokenSecret` or `oauth2`. + properties: + password: + description: |- + password defines a key of a Secret containing the password for + authentication. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + description: |- + username defines a key of a Secret containing the username for + authentication. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + bearerTokenSecret: + description: |- + bearerTokenSecret defines a key of a Secret containing the bearer token + used by the client for authentication. The secret needs to be in the + same namespace as the custom resource and readable by the Prometheus + Operator. + + Cannot be set at the same time as `authorization`, `basicAuth` or `oauth2`. + + Deprecated: use `authorization` instead. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + enableHttp2: + description: enableHttp2 can be used to disable HTTP2. + type: boolean + filterRunning: + description: |- + filterRunning when true, the pods which are not running (e.g. either in Failed or + Succeeded state) are dropped during the target discovery. + + If unset, the filtering is enabled. + + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase + type: boolean + followRedirects: + description: |- + followRedirects defines whether the client should follow HTTP 3xx + redirects. + type: boolean + honorLabels: + description: |- + honorLabels when true preserves the metric's labels when they collide + with the target's labels. + type: boolean + honorTimestamps: + description: |- + honorTimestamps defines whether Prometheus preserves the timestamps + when exposed by the target. + type: boolean + interval: + description: |- + interval at which Prometheus scrapes the metrics from the target. + + If empty, Prometheus uses the global scrape 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: + description: |- + metricRelabelings defines the relabeling rules to apply to the + samples before ingestion. + items: + description: |- + RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + scraped samples and remote write samples. + + More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + properties: + action: + default: replace + description: |- + action to perform based on the regex matching. + + `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + + 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: + description: |- + modulus to take of the hash of the source label values. + + Only applicable when the action is `HashMod`. + format: int64 + type: integer + regex: + description: regex defines the regular expression against + which the extracted value is matched. + type: string + replacement: + description: |- + replacement value against which a Replace action is performed if the + regular expression matches. + + Regex capture groups are available. + type: string + separator: + description: separator defines the string between concatenated + SourceLabels. + type: string + sourceLabels: + description: |- + sourceLabels defines the source labels select values from existing labels. Their content is + concatenated using the configured Separator and matched against the + configured regular expression. + items: + description: |- + LabelName is a valid Prometheus label name. + For Prometheus 3.x, a label name is valid if it contains UTF-8 characters. + For Prometheus 2.x, a label name is only valid if it contains ASCII characters, letters, numbers, as well as underscores. + type: string + type: array + targetLabel: + description: |- + targetLabel defines the label to which the resulting string is written in a replacement. + + It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + `KeepEqual` and `DropEqual` actions. + + Regex capture groups are available. + type: string + type: object + type: array + noProxy: + description: |- + noProxy defines a comma-separated string that can contain IPs, CIDR notation, domain names + that should be excluded from proxying. IP and domain names can + contain port numbers. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: string + oauth2: + description: |- + oauth2 defines the OAuth2 settings used by the client. + + It requires Prometheus >= 2.27.0. + + Cannot be set at the same time as `authorization`, `basicAuth` or `bearerTokenSecret`. + properties: + clientId: + description: |- + clientId defines a key of a Secret or ConfigMap containing the + OAuth2 client's ID. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing data + to use for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + clientSecret: + description: |- + clientSecret defines a key of a Secret containing the OAuth2 + client's secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + endpointParams: + additionalProperties: + type: string + description: |- + endpointParams configures the HTTP parameters to append to the token + URL. + type: object + noProxy: + description: |- + noProxy defines a comma-separated string that can contain IPs, CIDR notation, domain names + that should be excluded from proxying. IP and domain names can + contain port numbers. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: string + proxyConnectHeader: + additionalProperties: + items: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: array + description: |- + proxyConnectHeader optionally specifies headers to send to + proxies during CONNECT requests. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: object + x-kubernetes-map-type: atomic + proxyFromEnvironment: + description: |- + proxyFromEnvironment defines whether to use the proxy configuration defined by environment variables (HTTP_PROXY, HTTPS_PROXY, and NO_PROXY). + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: boolean + proxyUrl: + description: proxyUrl defines the HTTP proxy server to use. + pattern: ^(http|https|socks5)://.+$ + type: string + scopes: + description: scopes defines the OAuth2 scopes used for the + token request. + items: + type: string + type: array + tlsConfig: + description: |- + tlsConfig defines the TLS configuration to use when connecting to the OAuth2 server. + It requires Prometheus >= v2.43.0. + properties: + ca: + description: ca defines the Certificate authority used + when verifying server certificates. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing + data to use for the targets. + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + cert: + description: cert defines the Client certificate to + present when doing client-authentication. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing + data to use for the targets. + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + insecureSkipVerify: + description: insecureSkipVerify defines how to disable + target certificate validation. + type: boolean + keySecret: + description: keySecret defines the Secret containing + the client key file for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + maxVersion: + description: |- + maxVersion defines the maximum acceptable TLS version. + + It requires Prometheus >= v2.41.0 or Thanos >= v0.31.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + minVersion: + description: |- + minVersion defines the minimum acceptable TLS version. + + It requires Prometheus >= v2.35.0 or Thanos >= v0.28.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + serverName: + description: serverName is used to verify the hostname + for the targets. + type: string + type: object + tokenUrl: + description: tokenUrl defines the URL to fetch the token + from. + pattern: ^(http|https)://.+$ + type: string + required: + - clientId + - clientSecret + - tokenUrl + type: object + params: + additionalProperties: + items: + type: string + type: array + description: params define optional HTTP URL parameters. + type: object + path: + description: |- + path defines the HTTP path from which to scrape for metrics. + + If empty, Prometheus uses the default value (e.g. `/metrics`). + type: string + port: + description: |- + port defines the `Pod` port name which exposes the endpoint. + + If the pod doesn't expose a port with the same name, it will result + in no targets being discovered. + + If a `Pod` has multiple `Port`s with the same name (which is not + recommended), one target instance per unique port number will be + generated. + + It takes precedence over the `portNumber` and `targetPort` fields. + type: string + portNumber: + description: |- + portNumber defines the `Pod` port number which exposes the endpoint. + + The `Pod` must declare the specified `Port` in its spec or the + target will be dropped by Prometheus. + + This cannot be used to enable scraping of an undeclared port. + To scrape targets on a port which isn't exposed, you need to use + relabeling to override the `__address__` label (but beware of + duplicate targets if the `Pod` has other declared ports). + + In practice Prometheus will select targets for which the + matches the target's __meta_kubernetes_pod_container_port_number. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + proxyConnectHeader: + additionalProperties: + items: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: array + description: |- + proxyConnectHeader optionally specifies headers to send to + proxies during CONNECT requests. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: object + x-kubernetes-map-type: atomic + proxyFromEnvironment: + description: |- + proxyFromEnvironment defines whether to use the proxy configuration defined by environment variables (HTTP_PROXY, HTTPS_PROXY, and NO_PROXY). + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: boolean + proxyUrl: + description: proxyUrl defines the HTTP proxy server to use. + pattern: ^(http|https|socks5)://.+$ + type: string + relabelings: + description: |- + relabelings defines the relabeling rules to apply the target's + metadata labels. + + The Operator automatically adds relabelings for a few standard Kubernetes fields. + + The original scrape job's name is available via the `__tmp_prometheus_job_name` label. + + More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + items: + description: |- + RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + scraped samples and remote write samples. + + More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + properties: + action: + default: replace + description: |- + action to perform based on the regex matching. + + `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + + 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: + description: |- + modulus to take of the hash of the source label values. + + Only applicable when the action is `HashMod`. + format: int64 + type: integer + regex: + description: regex defines the regular expression against + which the extracted value is matched. + type: string + replacement: + description: |- + replacement value against which a Replace action is performed if the + regular expression matches. + + Regex capture groups are available. + type: string + separator: + description: separator defines the string between concatenated + SourceLabels. + type: string + sourceLabels: + description: |- + sourceLabels defines the source labels select values from existing labels. Their content is + concatenated using the configured Separator and matched against the + configured regular expression. + items: + description: |- + LabelName is a valid Prometheus label name. + For Prometheus 3.x, a label name is valid if it contains UTF-8 characters. + For Prometheus 2.x, a label name is only valid if it contains ASCII characters, letters, numbers, as well as underscores. + type: string + type: array + targetLabel: + description: |- + targetLabel defines the label to which the resulting string is written in a replacement. + + It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + `KeepEqual` and `DropEqual` actions. + + Regex capture groups are available. + type: string + type: object + type: array + scheme: + description: scheme defines the HTTP scheme to use for scraping. + enum: + - http + - https + - HTTP + - HTTPS + type: string + scrapeTimeout: + description: |- + scrapeTimeout defines the timeout after which Prometheus considers the scrape to be failed. + + If empty, Prometheus uses the global scrape timeout unless it is less + than the target's scrape interval value in which the latter is used. + The value cannot be greater than the scrape interval otherwise the operator will reject the resource. + 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 + description: |- + targetPort defines the name or number of the target port of the `Pod` object behind the Service, the + port must be specified with container port property. + + Deprecated: use 'port' or 'portNumber' instead. + x-kubernetes-int-or-string: true + tlsConfig: + description: tlsConfig defines the TLS configuration used by + the client. + properties: + ca: + description: ca defines the Certificate authority used when + verifying server certificates. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing data + to use for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + cert: + description: cert defines the Client certificate to present + when doing client-authentication. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing data + to use for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + insecureSkipVerify: + description: insecureSkipVerify defines how to disable target + certificate validation. + type: boolean + keySecret: + description: keySecret defines the Secret containing the + client key file for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + maxVersion: + description: |- + maxVersion defines the maximum acceptable TLS version. + + It requires Prometheus >= v2.41.0 or Thanos >= v0.31.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + minVersion: + description: |- + minVersion defines the minimum acceptable TLS version. + + It requires Prometheus >= v2.35.0 or Thanos >= v0.28.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + serverName: + description: serverName is used to verify the hostname for + the targets. + type: string + type: object + trackTimestampsStaleness: + description: |- + trackTimestampsStaleness defines whether Prometheus tracks staleness of + the metrics that have an explicit timestamp present in scraped data. + Has no effect if `honorTimestamps` is false. + + It requires Prometheus >= v2.48.0. + type: boolean + type: object + type: array + podTargetLabels: + description: |- + podTargetLabels defines the labels which are transferred from the + associated Kubernetes `Pod` object onto the ingested metrics. + items: + type: string + type: array + sampleLimit: + description: |- + sampleLimit defines a per-scrape limit on the number of scraped samples + that will be accepted. + format: int64 + type: integer + scrapeClass: + description: scrapeClass defines the scrape class to apply. + minLength: 1 + type: string + scrapeClassicHistograms: + description: |- + scrapeClassicHistograms defines whether to scrape a classic histogram that is also exposed as a native histogram. + It requires Prometheus >= v2.45.0. + + Notice: `scrapeClassicHistograms` corresponds to the `always_scrape_classic_histograms` field in the Prometheus configuration. + type: boolean + scrapeNativeHistograms: + description: |- + scrapeNativeHistograms defines whether to enable scraping of native histograms. + It requires Prometheus >= v3.8.0. + type: boolean + scrapeProtocols: + description: |- + scrapeProtocols defines the protocols to negotiate during a scrape. It tells clients the + protocols supported by Prometheus in order of preference (from most to least preferred). + + If unset, Prometheus uses its default value. + + It requires Prometheus >= v2.49.0. + items: + description: |- + ScrapeProtocol represents a protocol used by Prometheus for scraping metrics. + Supported values are: + * `OpenMetricsText0.0.1` + * `OpenMetricsText1.0.0` + * `PrometheusProto` + * `PrometheusText0.0.4` + * `PrometheusText1.0.0` + enum: + - PrometheusProto + - OpenMetricsText0.0.1 + - OpenMetricsText1.0.0 + - PrometheusText0.0.4 + - PrometheusText1.0.0 + type: string + type: array + x-kubernetes-list-type: set + selector: + description: selector defines the label selector to select the Kubernetes + `Pod` objects to scrape metrics from. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + 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 + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + selectorMechanism: + description: |- + selectorMechanism defines the mechanism used to select the endpoints to scrape. + By default, the selection process relies on relabel configurations to filter the discovered targets. + Alternatively, you can opt in for role selectors, which may offer better efficiency in large clusters. + Which strategy is best for your use case needs to be carefully evaluated. + + It requires Prometheus >= v2.17.0. + enum: + - RelabelConfig + - RoleSelector + type: string + targetLimit: + description: |- + targetLimit defines a limit on the number of scraped targets that will + be accepted. + format: int64 + type: integer + required: + - selector + type: object + status: + description: |- + status defines the status subresource. It is under active development and is updated only when the + "StatusForConfigurationResources" feature gate is enabled. + + Most recent observed status of the PodMonitor. Read-only. + More info: + https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + bindings: + description: bindings defines the list of workload resources (Prometheus, + PrometheusAgent, ThanosRuler or Alertmanager) which select the configuration + resource. + items: + description: WorkloadBinding is a link between a configuration resource + and a workload resource. + properties: + conditions: + description: conditions defines the current state of the configuration + resource when bound to the referenced Workload object. + items: + description: ConfigResourceCondition describes the status + of configuration resources linked to Prometheus, PrometheusAgent, + Alertmanager or ThanosRuler. + properties: + lastTransitionTime: + description: lastTransitionTime defines the time of the + last update to the current status property. + format: date-time + type: string + message: + description: message defines the human-readable message + indicating details for the condition's last transition. + type: string + observedGeneration: + description: |- + observedGeneration defines the .metadata.generation that the + condition was set based upon. For instance, if `.metadata.generation` is + currently 12, but the `.status.conditions[].observedGeneration` is 9, the + condition is out of date with respect to the current state of the object. + format: int64 + type: integer + reason: + description: reason for the condition's last transition. + type: string + status: + description: status of the condition. + minLength: 1 + type: string + type: + description: |- + type of the condition being reported. + Currently, only "Accepted" is supported. + 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: + description: group defines the group of the referenced resource. + enum: + - monitoring.coreos.com + type: string + name: + description: name defines the name of the referenced object. + minLength: 1 + type: string + namespace: + description: namespace defines the namespace of the referenced + object. + minLength: 1 + type: string + resource: + description: resource defines the type of resource being referenced + (e.g. Prometheus, PrometheusAgent, ThanosRuler or Alertmanager). + enum: + - prometheuses + - prometheusagents + - thanosrulers + - alertmanagers + type: string + required: + - group + - name + - namespace + - resource + type: object + type: array + x-kubernetes-list-map-keys: + - group + - resource + - name + - namespace + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/crdmanager/assets/monitoring.coreos.com_servicemonitors.yaml b/pkg/crdmanager/assets/monitoring.coreos.com_servicemonitors.yaml new file mode 100644 index 000000000..a0478c098 --- /dev/null +++ b/pkg/crdmanager/assets/monitoring.coreos.com_servicemonitors.yaml @@ -0,0 +1,1419 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + operator.prometheus.io/version: 0.92.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: + description: |- + The `ServiceMonitor` custom resource definition (CRD) defines how `Prometheus` and `PrometheusAgent` can scrape metrics from a group of services. + Among other things, it allows to specify: + * The services to scrape via label selectors. + * The container ports to scrape. + * Authentication credentials to use. + * Target and metric relabeling. + + `Prometheus` and `PrometheusAgent` objects select `ServiceMonitor` objects using label and namespace selectors. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + spec defines the specification of desired Service selection for target discovery by + Prometheus. + properties: + attachMetadata: + description: |- + attachMetadata defines additional metadata which is added to the + discovered targets. + + It requires Prometheus >= v2.37.0. + properties: + node: + description: |- + node when set to true, Prometheus attaches node metadata to the discovered + targets. + + The Prometheus service account must have the `list` and `watch` + permissions on the `Nodes` objects. + + Node metadata labels are not automatically added to scraped metrics. They are + exposed as `__meta_kubernetes_node_*` labels and can be copied to timeseries + with relabeling configuration. + type: boolean + type: object + bodySizeLimit: + description: |- + bodySizeLimit when defined, bodySizeLimit specifies a job level limit on the size + of uncompressed response body that will be accepted by Prometheus. + + It requires Prometheus >= v2.28.0. + pattern: (^0|([0-9]*[.])?[0-9]+((K|M|G|T|E|P)i?)?B)$ + type: string + convertClassicHistogramsToNHCB: + description: |- + convertClassicHistogramsToNHCB defines whether to convert all scraped classic histograms into a native histogram with custom buckets. + It requires Prometheus >= v3.0.0. + type: boolean + endpoints: + description: |- + endpoints defines the list of endpoints part of this ServiceMonitor. + Defines how to scrape metrics from Kubernetes [Endpoints](https://kubernetes.io/docs/concepts/services-networking/service/#endpoints) objects. + In most cases, an Endpoints object is backed by a Kubernetes [Service](https://kubernetes.io/docs/concepts/services-networking/service/) object with the same name and labels. + items: + description: |- + Endpoint defines an endpoint serving Prometheus metrics to be scraped by + Prometheus. + properties: + authorization: + description: |- + authorization configures the Authorization header credentials used by + the client. + + Cannot be set at the same time as `basicAuth`, `bearerTokenSecret` or `oauth2`. + properties: + credentials: + description: credentials defines a key of a Secret in the + namespace that contains the credentials for authentication. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: + description: |- + type defines the authentication type. The value is case-insensitive. + + "Basic" is not a supported value. + + Default: "Bearer" + type: string + type: object + basicAuth: + description: |- + basicAuth defines the Basic Authentication credentials used by the + client. + + Cannot be set at the same time as `authorization`, `bearerTokenSecret` or `oauth2`. + properties: + password: + description: |- + password defines a key of a Secret containing the password for + authentication. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + username: + description: |- + username defines a key of a Secret containing the username for + authentication. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + bearerTokenFile: + description: |- + bearerTokenFile defines the file to read bearer token for scraping the target. + + Deprecated: use `authorization` instead. + type: string + bearerTokenSecret: + description: |- + bearerTokenSecret defines a key of a Secret containing the bearer token + used by the client for authentication. The secret needs to be in the + same namespace as the custom resource and readable by the Prometheus + Operator. + + Cannot be set at the same time as `authorization`, `basicAuth` or `oauth2`. + + Deprecated: use `authorization` instead. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + enableHttp2: + description: enableHttp2 can be used to disable HTTP2. + type: boolean + filterRunning: + description: |- + filterRunning when true, the pods which are not running (e.g. either in Failed or + Succeeded state) are dropped during the target discovery. + + If unset, the filtering is enabled. + + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase + type: boolean + followRedirects: + description: |- + followRedirects defines whether the client should follow HTTP 3xx + redirects. + type: boolean + honorLabels: + description: |- + honorLabels defines when true the metric's labels when they collide + with the target's labels. + type: boolean + honorTimestamps: + description: |- + honorTimestamps defines whether Prometheus preserves the timestamps + when exposed by the target. + type: boolean + interval: + description: |- + interval at which Prometheus scrapes the metrics from the target. + + If empty, Prometheus uses the global scrape 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: + description: |- + metricRelabelings defines the relabeling rules to apply to the + samples before ingestion. + items: + description: |- + RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + scraped samples and remote write samples. + + More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + properties: + action: + default: replace + description: |- + action to perform based on the regex matching. + + `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + + 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: + description: |- + modulus to take of the hash of the source label values. + + Only applicable when the action is `HashMod`. + format: int64 + type: integer + regex: + description: regex defines the regular expression against + which the extracted value is matched. + type: string + replacement: + description: |- + replacement value against which a Replace action is performed if the + regular expression matches. + + Regex capture groups are available. + type: string + separator: + description: separator defines the string between concatenated + SourceLabels. + type: string + sourceLabels: + description: |- + sourceLabels defines the source labels select values from existing labels. Their content is + concatenated using the configured Separator and matched against the + configured regular expression. + items: + description: |- + LabelName is a valid Prometheus label name. + For Prometheus 3.x, a label name is valid if it contains UTF-8 characters. + For Prometheus 2.x, a label name is only valid if it contains ASCII characters, letters, numbers, as well as underscores. + type: string + type: array + targetLabel: + description: |- + targetLabel defines the label to which the resulting string is written in a replacement. + + It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + `KeepEqual` and `DropEqual` actions. + + Regex capture groups are available. + type: string + type: object + type: array + noProxy: + description: |- + noProxy defines a comma-separated string that can contain IPs, CIDR notation, domain names + that should be excluded from proxying. IP and domain names can + contain port numbers. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: string + oauth2: + description: |- + oauth2 defines the OAuth2 settings used by the client. + + It requires Prometheus >= 2.27.0. + + Cannot be set at the same time as `authorization`, `basicAuth` or `bearerTokenSecret`. + properties: + clientId: + description: |- + clientId defines a key of a Secret or ConfigMap containing the + OAuth2 client's ID. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing data + to use for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + clientSecret: + description: |- + clientSecret defines a key of a Secret containing the OAuth2 + client's secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + endpointParams: + additionalProperties: + type: string + description: |- + endpointParams configures the HTTP parameters to append to the token + URL. + type: object + noProxy: + description: |- + noProxy defines a comma-separated string that can contain IPs, CIDR notation, domain names + that should be excluded from proxying. IP and domain names can + contain port numbers. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: string + proxyConnectHeader: + additionalProperties: + items: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: array + description: |- + proxyConnectHeader optionally specifies headers to send to + proxies during CONNECT requests. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: object + x-kubernetes-map-type: atomic + proxyFromEnvironment: + description: |- + proxyFromEnvironment defines whether to use the proxy configuration defined by environment variables (HTTP_PROXY, HTTPS_PROXY, and NO_PROXY). + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: boolean + proxyUrl: + description: proxyUrl defines the HTTP proxy server to use. + pattern: ^(http|https|socks5)://.+$ + type: string + scopes: + description: scopes defines the OAuth2 scopes used for the + token request. + items: + type: string + type: array + tlsConfig: + description: |- + tlsConfig defines the TLS configuration to use when connecting to the OAuth2 server. + It requires Prometheus >= v2.43.0. + properties: + ca: + description: ca defines the Certificate authority used + when verifying server certificates. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing + data to use for the targets. + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + cert: + description: cert defines the Client certificate to + present when doing client-authentication. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or + its key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing + data to use for the targets. + properties: + key: + description: The key of the secret to select + from. Must be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + insecureSkipVerify: + description: insecureSkipVerify defines how to disable + target certificate validation. + type: boolean + keySecret: + description: keySecret defines the Secret containing + the client key file for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + maxVersion: + description: |- + maxVersion defines the maximum acceptable TLS version. + + It requires Prometheus >= v2.41.0 or Thanos >= v0.31.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + minVersion: + description: |- + minVersion defines the minimum acceptable TLS version. + + It requires Prometheus >= v2.35.0 or Thanos >= v0.28.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + serverName: + description: serverName is used to verify the hostname + for the targets. + type: string + type: object + tokenUrl: + description: tokenUrl defines the URL to fetch the token + from. + pattern: ^(http|https)://.+$ + type: string + required: + - clientId + - clientSecret + - tokenUrl + type: object + params: + additionalProperties: + items: + type: string + type: array + description: params define optional HTTP URL parameters. + type: object + path: + description: |- + path defines the HTTP path from which to scrape for metrics. + + If empty, Prometheus uses the default value (e.g. `/metrics`). + type: string + port: + description: |- + port defines the name of the Service port which this endpoint refers to + (e.g. `.spec.ports[].name`). + + It takes precedence over `targetPort`. + type: string + proxyConnectHeader: + additionalProperties: + items: + description: SecretKeySelector selects a key of a Secret. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: array + description: |- + proxyConnectHeader optionally specifies headers to send to + proxies during CONNECT requests. + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: object + x-kubernetes-map-type: atomic + proxyFromEnvironment: + description: |- + proxyFromEnvironment defines whether to use the proxy configuration defined by environment variables (HTTP_PROXY, HTTPS_PROXY, and NO_PROXY). + + It requires Prometheus >= v2.43.0, Alertmanager >= v0.25.0 or Thanos >= v0.32.0. + type: boolean + proxyUrl: + description: proxyUrl defines the HTTP proxy server to use. + pattern: ^(http|https|socks5)://.+$ + type: string + relabelings: + description: |- + relabelings defines the relabeling rules to apply the target's + metadata labels. + + The Operator automatically adds relabelings for a few standard Kubernetes fields. + + The original scrape job's name is available via the `__tmp_prometheus_job_name` label. + + More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + items: + description: |- + RelabelConfig allows dynamic rewriting of the label set for targets, alerts, + scraped samples and remote write samples. + + More info: https://prometheus.io/docs/prometheus/latest/configuration/configuration/#relabel_config + properties: + action: + default: replace + description: |- + action to perform based on the regex matching. + + `Uppercase` and `Lowercase` actions require Prometheus >= v2.36.0. + `DropEqual` and `KeepEqual` actions require Prometheus >= v2.41.0. + + 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: + description: |- + modulus to take of the hash of the source label values. + + Only applicable when the action is `HashMod`. + format: int64 + type: integer + regex: + description: regex defines the regular expression against + which the extracted value is matched. + type: string + replacement: + description: |- + replacement value against which a Replace action is performed if the + regular expression matches. + + Regex capture groups are available. + type: string + separator: + description: separator defines the string between concatenated + SourceLabels. + type: string + sourceLabels: + description: |- + sourceLabels defines the source labels select values from existing labels. Their content is + concatenated using the configured Separator and matched against the + configured regular expression. + items: + description: |- + LabelName is a valid Prometheus label name. + For Prometheus 3.x, a label name is valid if it contains UTF-8 characters. + For Prometheus 2.x, a label name is only valid if it contains ASCII characters, letters, numbers, as well as underscores. + type: string + type: array + targetLabel: + description: |- + targetLabel defines the label to which the resulting string is written in a replacement. + + It is mandatory for `Replace`, `HashMod`, `Lowercase`, `Uppercase`, + `KeepEqual` and `DropEqual` actions. + + Regex capture groups are available. + type: string + type: object + type: array + scheme: + description: scheme defines the HTTP scheme to use when scraping + the metrics. + enum: + - http + - https + - HTTP + - HTTPS + type: string + scrapeTimeout: + description: |- + scrapeTimeout defines the timeout after which Prometheus considers the scrape to be failed. + + If empty, Prometheus uses the global scrape timeout unless it is less + than the target's scrape interval value in which the latter is used. + The value cannot be greater than the scrape interval otherwise the operator will reject the resource. + 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 + description: |- + targetPort defines the name or number of a container port on Pods selected + by the Service. + If a name, it matches against `.spec.containers[].ports[].name` of the Pods. + If a number, it matches against `.spec.containers[].ports[].containerPort` of the Pods. + x-kubernetes-int-or-string: true + tlsConfig: + description: tlsConfig defines TLS configuration used by the + client. + properties: + ca: + description: ca defines the Certificate authority used when + verifying server certificates. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing data + to use for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + caFile: + description: caFile defines the path to the CA cert in the + Prometheus container to use for the targets. + type: string + cert: + description: cert defines the Client certificate to present + when doing client-authentication. + properties: + configMap: + description: configMap defines the ConfigMap containing + data to use for the targets. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + secret: + description: secret defines the Secret containing data + to use for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + certFile: + description: certFile defines the path to the client cert + file in the Prometheus container for the targets. + type: string + insecureSkipVerify: + description: insecureSkipVerify defines how to disable target + certificate validation. + type: boolean + keyFile: + description: keyFile defines the path to the client key + file in the Prometheus container for the targets. + type: string + keySecret: + description: keySecret defines the Secret containing the + client key file for the targets. + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key must + be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + maxVersion: + description: |- + maxVersion defines the maximum acceptable TLS version. + + It requires Prometheus >= v2.41.0 or Thanos >= v0.31.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + minVersion: + description: |- + minVersion defines the minimum acceptable TLS version. + + It requires Prometheus >= v2.35.0 or Thanos >= v0.28.0. + enum: + - TLS10 + - TLS11 + - TLS12 + - TLS13 + type: string + serverName: + description: serverName is used to verify the hostname for + the targets. + type: string + type: object + trackTimestampsStaleness: + description: |- + trackTimestampsStaleness defines whether Prometheus tracks staleness of + the metrics that have an explicit timestamp present in scraped data. + Has no effect if `honorTimestamps` is false. + + It requires Prometheus >= v2.48.0. + type: boolean + type: object + type: array + fallbackScrapeProtocol: + description: |- + fallbackScrapeProtocol defines the protocol to use if a scrape returns blank, unparseable, or otherwise invalid Content-Type. + + It requires Prometheus >= v3.0.0. + enum: + - PrometheusProto + - OpenMetricsText0.0.1 + - OpenMetricsText1.0.0 + - PrometheusText0.0.4 + - PrometheusText1.0.0 + type: string + jobLabel: + description: |- + jobLabel selects the label from the associated Kubernetes `Service` + object which will be used as the `job` label for all metrics. + + For example if `jobLabel` is set to `foo` and the Kubernetes `Service` + object is labeled with `foo: bar`, then Prometheus adds the `job="bar"` + label to all ingested metrics. + + If the value of this field is empty or if the label doesn't exist for + the given Service, the `job` label of the metrics defaults to the name + of the associated Kubernetes `Service`. + type: string + keepDroppedTargets: + description: |- + keepDroppedTargets defines the per-scrape limit on the number of targets dropped by relabeling + that will be kept in memory. 0 means no limit. + + It requires Prometheus >= v2.47.0. + format: int64 + type: integer + labelLimit: + description: |- + labelLimit defines the per-scrape limit on number of labels that will be accepted for a sample. + + It requires Prometheus >= v2.27.0. + format: int64 + type: integer + labelNameLengthLimit: + description: |- + labelNameLengthLimit defines the per-scrape limit on length of labels name that will be accepted for a sample. + + It requires Prometheus >= v2.27.0. + format: int64 + type: integer + labelValueLengthLimit: + description: |- + labelValueLengthLimit defines the per-scrape limit on length of labels value that will be accepted for a sample. + + It requires Prometheus >= v2.27.0. + format: int64 + type: integer + namespaceSelector: + description: |- + namespaceSelector defines in which namespace(s) Prometheus should discover the services. + By default, the services are discovered in the same namespace as the `ServiceMonitor` object but it is possible to select pods across different/all namespaces. + properties: + any: + description: |- + any defines the boolean describing whether all namespaces are selected in contrast to a + list restricting them. + type: boolean + matchNames: + description: matchNames defines the list of namespace names to + select from. + items: + type: string + type: array + type: object + nativeHistogramBucketLimit: + description: |- + nativeHistogramBucketLimit defines ff there are more than this many buckets in a native histogram, + buckets will be merged to stay within the limit. + It requires Prometheus >= v2.45.0. + format: int64 + type: integer + nativeHistogramMinBucketFactor: + anyOf: + - type: integer + - type: string + description: |- + nativeHistogramMinBucketFactor defines if the growth factor of one bucket to the next is smaller than this, + buckets will be merged to increase the factor sufficiently. + It requires Prometheus >= v2.50.0. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + podTargetLabels: + description: |- + podTargetLabels defines the labels which are transferred from the + associated Kubernetes `Pod` object onto the ingested metrics. + items: + type: string + type: array + sampleLimit: + description: |- + sampleLimit defines a per-scrape limit on the number of scraped samples + that will be accepted. + format: int64 + type: integer + scrapeClass: + description: scrapeClass defines the scrape class to apply. + minLength: 1 + type: string + scrapeClassicHistograms: + description: |- + scrapeClassicHistograms defines whether to scrape a classic histogram that is also exposed as a native histogram. + It requires Prometheus >= v2.45.0. + + Notice: `scrapeClassicHistograms` corresponds to the `always_scrape_classic_histograms` field in the Prometheus configuration. + type: boolean + scrapeNativeHistograms: + description: |- + scrapeNativeHistograms defines whether to enable scraping of native histograms. + It requires Prometheus >= v3.8.0. + type: boolean + scrapeProtocols: + description: |- + scrapeProtocols defines the protocols to negotiate during a scrape. It tells clients the + protocols supported by Prometheus in order of preference (from most to least preferred). + + If unset, Prometheus uses its default value. + + It requires Prometheus >= v2.49.0. + items: + description: |- + ScrapeProtocol represents a protocol used by Prometheus for scraping metrics. + Supported values are: + * `OpenMetricsText0.0.1` + * `OpenMetricsText1.0.0` + * `PrometheusProto` + * `PrometheusText0.0.4` + * `PrometheusText1.0.0` + enum: + - PrometheusProto + - OpenMetricsText0.0.1 + - OpenMetricsText1.0.0 + - PrometheusText0.0.4 + - PrometheusText1.0.0 + type: string + type: array + x-kubernetes-list-type: set + selector: + description: selector defines the label selector to select the Kubernetes + `Endpoints` objects to scrape metrics from. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. + The requirements are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector applies + to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + 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 + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + selectorMechanism: + description: |- + selectorMechanism defines the mechanism used to select the endpoints to scrape. + By default, the selection process relies on relabel configurations to filter the discovered targets. + Alternatively, you can opt in for role selectors, which may offer better efficiency in large clusters. + Which strategy is best for your use case needs to be carefully evaluated. + + It requires Prometheus >= v2.17.0. + enum: + - RelabelConfig + - RoleSelector + type: string + serviceDiscoveryRole: + description: |- + serviceDiscoveryRole defines the service discovery role used to discover targets. + + If set, the value should be either "Endpoints" or "EndpointSlice". + Otherwise it defaults to the value defined in the + Prometheus/PrometheusAgent resource. + enum: + - Endpoints + - EndpointSlice + type: string + targetLabels: + description: |- + targetLabels defines the labels which are transferred from the + associated Kubernetes `Service` object onto the ingested metrics. + items: + type: string + type: array + targetLimit: + description: |- + targetLimit defines a limit on the number of scraped targets that will + be accepted. + format: int64 + type: integer + required: + - endpoints + - selector + type: object + status: + description: |- + status defines the status subresource. It is under active development and is updated only when the + "StatusForConfigurationResources" feature gate is enabled. + + Most recent observed status of the ServiceMonitor. Read-only. + More info: + https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + bindings: + description: bindings defines the list of workload resources (Prometheus, + PrometheusAgent, ThanosRuler or Alertmanager) which select the configuration + resource. + items: + description: WorkloadBinding is a link between a configuration resource + and a workload resource. + properties: + conditions: + description: conditions defines the current state of the configuration + resource when bound to the referenced Workload object. + items: + description: ConfigResourceCondition describes the status + of configuration resources linked to Prometheus, PrometheusAgent, + Alertmanager or ThanosRuler. + properties: + lastTransitionTime: + description: lastTransitionTime defines the time of the + last update to the current status property. + format: date-time + type: string + message: + description: message defines the human-readable message + indicating details for the condition's last transition. + type: string + observedGeneration: + description: |- + observedGeneration defines the .metadata.generation that the + condition was set based upon. For instance, if `.metadata.generation` is + currently 12, but the `.status.conditions[].observedGeneration` is 9, the + condition is out of date with respect to the current state of the object. + format: int64 + type: integer + reason: + description: reason for the condition's last transition. + type: string + status: + description: status of the condition. + minLength: 1 + type: string + type: + description: |- + type of the condition being reported. + Currently, only "Accepted" is supported. + 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: + description: group defines the group of the referenced resource. + enum: + - monitoring.coreos.com + type: string + name: + description: name defines the name of the referenced object. + minLength: 1 + type: string + namespace: + description: namespace defines the namespace of the referenced + object. + minLength: 1 + type: string + resource: + description: resource defines the type of resource being referenced + (e.g. Prometheus, PrometheusAgent, ThanosRuler or Alertmanager). + enum: + - prometheuses + - prometheusagents + - thanosrulers + - alertmanagers + type: string + required: + - group + - name + - namespace + - resource + type: object + type: array + x-kubernetes-list-map-keys: + - group + - resource + - name + - namespace + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/pkg/crdmanager/crdmanager_suite_test.go b/pkg/crdmanager/crdmanager_suite_test.go new file mode 100644 index 000000000..c85fa76fd --- /dev/null +++ b/pkg/crdmanager/crdmanager_suite_test.go @@ -0,0 +1,52 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crdmanager + +import ( + "fmt" + "os" + "testing" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" +) + +// envClient talks to a real API server (envtest), which maintains +// metadata.generation and metadata.managedFields the way a cluster does. The +// fake client used by the unit tests does not. +var ( + envClient client.WithWatch + envScheme = runtime.NewScheme() +) + +func TestMain(m *testing.M) { + testEnv := &envtest.Environment{} + cfg, err := testEnv.Start() + if err != nil { + fmt.Printf("failed to start testEnv: %v", err) + os.Exit(1) + } + for _, add := range []func(*runtime.Scheme) error{clientgoscheme.AddToScheme, apiextensionsv1.AddToScheme} { + if err = add(envScheme); err != nil { + fmt.Printf("failed to register scheme: %v", err) + os.Exit(1) + } + } + envClient, err = client.NewWithWatch(cfg, client.Options{Scheme: envScheme}) + if err != nil { + fmt.Printf("failed to set up a Kubernetes client: %v", err) + os.Exit(1) + } + + code := m.Run() + + if err = testEnv.Stop(); err != nil { + fmt.Printf("failed to stop testEnv: %v", err) + os.Exit(1) + } + os.Exit(code) +} diff --git a/pkg/crdmanager/envtest_test.go b/pkg/crdmanager/envtest_test.go new file mode 100644 index 000000000..64d7d3c09 --- /dev/null +++ b/pkg/crdmanager/envtest_test.go @@ -0,0 +1,359 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crdmanager + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +// These tests run the reconciler and IsOwned against a real API server +// (envtest). They cover the design's E1-E8 cases. + +var managedNames = []string{smName, pmName} + +// countingClient wraps envClient and counts every write the reconciler makes. +func countingClient(writes *int) client.WithWatch { + return interceptor.NewClient(envClient, interceptor.Funcs{ + Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + *writes++ + return c.Create(ctx, obj, opts...) + }, + Update: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { + *writes++ + return c.Update(ctx, obj, opts...) + }, + Patch: func(ctx context.Context, c client.WithWatch, obj client.Object, p client.Patch, opts ...client.PatchOption) error { + *writes++ + return c.Patch(ctx, obj, p, opts...) + }, + Apply: func(ctx context.Context, c client.WithWatch, obj runtime.ApplyConfiguration, opts ...client.ApplyOption) error { + *writes++ + return c.Apply(ctx, obj, opts...) + }, + Delete: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.DeleteOption) error { + *writes++ + return c.Delete(ctx, obj, opts...) + }, + }) +} + +// resetCRDs deletes both managed CRDs, if present, and waits until they are gone. +func resetCRDs(t *testing.T) { + t.Helper() + ctx := context.Background() + for _, name := range managedNames { + crd := &apiextensionsv1.CustomResourceDefinition{ObjectMeta: metav1.ObjectMeta{Name: name}} + if err := envClient.Delete(ctx, crd); err != nil && !apierrors.IsNotFound(err) { + require.NoError(t, err) + } + } + require.Eventually(t, func() bool { + for _, name := range managedNames { + err := envClient.Get(ctx, client.ObjectKey{Name: name}, &apiextensionsv1.CustomResourceDefinition{}) + if !apierrors.IsNotFound(err) { + return false + } + } + return true + }, 30*time.Second, 200*time.Millisecond, "CRDs were not deleted") +} + +// reconcileAll runs one reconcile per managed CRD. +func reconcileAll(t *testing.T, r *Reconciler) { + t.Helper() + for _, name := range managedNames { + _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: name}}) + require.NoError(t, err) + } +} + +func liveCRD(t *testing.T, name string) *apiextensionsv1.CustomResourceDefinition { + t.Helper() + crd := &apiextensionsv1.CustomResourceDefinition{} + require.NoError(t, envClient.Get(context.Background(), client.ObjectKey{Name: name}, crd)) + return crd +} + +// settledCRD waits until the API server has finished its own status writes to +// the CRD (NamesAccepted, then Established) and returns it. Those writes move +// resourceVersion after any create or spec change, so a test comparing +// resourceVersion must start from a settled CRD. +func settledCRD(t *testing.T, name string) *apiextensionsv1.CustomResourceDefinition { + t.Helper() + var crd *apiextensionsv1.CustomResourceDefinition + require.Eventually(t, func() bool { + first := liveCRD(t, name) + if !isEstablished(first) { + return false + } + time.Sleep(500 * time.Millisecond) + crd = liveCRD(t, name) + return crd.ResourceVersion == first.ResourceVersion + }, 30*time.Second, 100*time.Millisecond, "%s did not settle", name) + return crd +} + +func isEstablished(crd *apiextensionsv1.CustomResourceDefinition) bool { + for _, c := range crd.Status.Conditions { + if c.Type == apiextensionsv1.Established { + return c.Status == apiextensionsv1.ConditionTrue + } + } + return false +} + +// createdByOperator resets the cluster, lets the reconciler create both CRDs and +// returns the live ServiceMonitor CRD. +func createdByOperator(t *testing.T) *apiextensionsv1.CustomResourceDefinition { + t.Helper() + resetCRDs(t) + r, err := NewReconciler(envClient, logr.Discard(), record.NewFakeRecorder(10)) + require.NoError(t, err) + reconcileAll(t, r) + crd := liveCRD(t, smName) + owned, reason := IsOwned(crd) + require.True(t, owned, "precondition: a CRD the operator created must be owned: %s", reason) + return crd +} + +// specManagers returns the managers whose managedFields entry owns spec fields. +func specManagers(crd *apiextensionsv1.CustomResourceDefinition) []string { + var out []string + for _, mf := range crd.GetManagedFields() { + if ownsSpec(mf) { + out = append(out, mf.Manager) + } + } + return out +} + +// assertStepsBack checks E6: once the ownership check fails, repeated +// reconciles make zero writes and emit exactly one PrometheusCRDNotManaged Event. +func assertStepsBack(t *testing.T, wantReason string) { + t.Helper() + before := settledCRD(t, smName).ResourceVersion + + writes := 0 + rec := record.NewFakeRecorder(10) + r, err := NewReconciler(countingClient(&writes), logr.Discard(), rec) + require.NoError(t, err) + for i := 0; i < 3; i++ { + _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: smName}}) + require.NoError(t, err) + } + + assert.Zero(t, writes, "the operator wrote to a CRD it does not own") + assert.Equal(t, before, liveCRD(t, smName).ResourceVersion, "the CRD changed") + events := drain(rec) + require.Len(t, events, 1, "step-back must be reported exactly once") + assert.Contains(t, events[0], EventReasonNotManaged) + assert.Contains(t, events[0], wantReason) +} + +// E1 +func TestEnvtestCreatesBothCRDsOwned(t *testing.T) { + resetCRDs(t) + rec := record.NewFakeRecorder(10) + r, err := NewReconciler(envClient, logr.Discard(), rec) + require.NoError(t, err) + reconcileAll(t, r) + + for _, name := range managedNames { + crd := liveCRD(t, name) + assert.Equal(t, int64(1), crd.Generation, "%s generation", name) + assert.Equal(t, []string{FieldManager}, specManagers(crd), "%s spec managers", name) + owned, reason := IsOwned(crd) + assert.True(t, owned, "%s: %s", name, reason) + } + events := drain(rec) + require.Len(t, events, 2) + for _, e := range events { + assert.Contains(t, e, EventReasonCreated) + } +} + +// E2 +func TestEnvtestLabelOnlyChangeKeepsOwnership(t *testing.T) { + crd := createdByOperator(t) + patch := client.MergeFrom(crd.DeepCopy()) + crd.Labels["team"] = "observability" + require.NoError(t, envClient.Patch(context.Background(), crd, patch, client.FieldOwner("kubectl-label"))) + + live := liveCRD(t, smName) + assert.Equal(t, int64(1), live.Generation, "a label change must not move generation") + owned, reason := IsOwned(live) + assert.True(t, owned, reason) +} + +// E3 and E6 +func TestEnvtestForeignServerSideApplyStepsBack(t *testing.T) { + createdByOperator(t) + desired := desiredWithOwnership(desiredCRD(t, smName)) + u := applyConfig(t, desired) + require.NoError(t, envClient.Apply(context.Background(), client.ApplyConfigurationFromUnstructured(u), + client.FieldOwner("other-tool"), client.ForceOwnership)) + + live := liveCRD(t, smName) + assert.Contains(t, specManagers(live), "other-tool") + owned, reason := IsOwned(live) + assert.False(t, owned) + assert.Contains(t, reason, `"other-tool"`) + assertStepsBack(t, `"other-tool"`) +} + +// E4 and E6 +func TestEnvtestForeignSpecChangeStepsBack(t *testing.T) { + createdByOperator(t) + updateCRD(t, "other-client", func(crd *apiextensionsv1.CustomResourceDefinition) { + crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Description = "edited by another client" + }) + + live := liveCRD(t, smName) + assert.Equal(t, int64(2), live.Generation, "a spec change must move generation") + owned, reason := IsOwned(live) + assert.False(t, owned) + assert.Contains(t, reason, "metadata.generation") + assertStepsBack(t, "metadata.generation") +} + +// E5 and E6 +func TestEnvtestNewerUpstreamVersionStepsBack(t *testing.T) { + crd := createdByOperator(t) + patch := client.MergeFrom(crd.DeepCopy()) + crd.Annotations[UpstreamVersionAnnotation] = "0.94.2" + require.NoError(t, envClient.Patch(context.Background(), crd, patch, client.FieldOwner("other-client"))) + + owned, reason := IsOwned(liveCRD(t, smName)) + assert.False(t, owned) + assert.Contains(t, reason, UpstreamVersionAnnotation) + assertStepsBack(t, UpstreamVersionAnnotation) +} + +// E7 +func TestEnvtestDeletedCRDIsRecreatedOwned(t *testing.T) { + first := createdByOperator(t) + require.NoError(t, envClient.Delete(context.Background(), first)) + require.Eventually(t, func() bool { + err := envClient.Get(context.Background(), client.ObjectKey{Name: smName}, &apiextensionsv1.CustomResourceDefinition{}) + return apierrors.IsNotFound(err) + }, 30*time.Second, 200*time.Millisecond) + + r, err := NewReconciler(envClient, logr.Discard(), record.NewFakeRecorder(10)) + require.NoError(t, err) + reconcileAll(t, r) + + again := liveCRD(t, smName) + assert.NotEqual(t, first.UID, again.UID, "expected a new object") + assert.Equal(t, int64(1), again.Generation) + assert.Equal(t, "1", again.Annotations[AppliedGenerationAnnotation]) + owned, reason := IsOwned(again) + assert.True(t, owned, reason) +} + +// E8 and E6: a client-side kubectl apply with content identical to ours moves no +// spec field ownership and no generation, so only check 5 catches it. +func TestEnvtestClientSideApplyMarkerStepsBack(t *testing.T) { + createdByOperator(t) + lastApplied, err := json.Marshal(desiredWithOwnership(desiredCRD(t, smName))) + require.NoError(t, err) + updateCRD(t, "kubectl-client-side-apply", func(crd *apiextensionsv1.CustomResourceDefinition) { + crd.Annotations[lastAppliedAnnotation] = string(lastApplied) + }) + + live := liveCRD(t, smName) + assert.Equal(t, int64(1), live.Generation, "identical content must not move generation") + assert.Equal(t, []string{FieldManager}, specManagers(live), "identical content must not add a spec manager") + owned, reason := IsOwned(live) + assert.False(t, owned) + assert.Contains(t, reason, lastAppliedAnnotation) + assertStepsBack(t, lastAppliedAnnotation) +} + +// E8 and E6: Helm taking ownership (helm --take-ownership) adds its release +// annotations and managed-by label without changing spec. +func TestEnvtestHelmOwnershipMarkersStepBack(t *testing.T) { + crd := createdByOperator(t) + patch := client.MergeFrom(crd.DeepCopy()) + crd.Annotations[helmReleaseAnnotation] = "kube-prometheus-stack" + crd.Annotations["meta.helm.sh/release-namespace"] = "monitoring" + crd.Labels[appManagedByLabel] = "Helm" + require.NoError(t, envClient.Patch(context.Background(), crd, patch, client.FieldOwner("helm"))) + + live := liveCRD(t, smName) + assert.Equal(t, int64(1), live.Generation) + assert.Equal(t, []string{FieldManager}, specManagers(live)) + owned, reason := IsOwned(live) + assert.False(t, owned) + assert.Contains(t, reason, helmReleaseAnnotation) + assertStepsBack(t, helmReleaseAnnotation) +} + +// updateCRD re-reads the ServiceMonitor CRD, applies mutate and updates it as +// manager, retrying when the API server changed it in between (it sets status +// conditions right after a create). +func updateCRD(t *testing.T, manager string, mutate func(*apiextensionsv1.CustomResourceDefinition)) { + t.Helper() + require.NoError(t, retry.RetryOnConflict(retry.DefaultRetry, func() error { + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := envClient.Get(context.Background(), client.ObjectKey{Name: smName}, crd); err != nil { + return err + } + mutate(crd) + return envClient.Update(context.Background(), crd, client.FieldOwner(manager)) + })) +} + +func desiredCRD(t *testing.T, name string) *apiextensionsv1.CustomResourceDefinition { + t.Helper() + crds, err := bundledCRDs() + require.NoError(t, err) + for _, crd := range crds { + if crd.Name == name { + return crd + } + } + t.Fatalf("no embedded CRD %s", name) + return nil +} + +// applyConfig converts a CRD into an unstructured apply configuration with only +// the fields a manifest would carry. +func applyConfig(t *testing.T, crd *apiextensionsv1.CustomResourceDefinition) *unstructured.Unstructured { + t.Helper() + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(crd) + require.NoError(t, err) + u := &unstructured.Unstructured{Object: obj} + u.SetAPIVersion(apiextensionsv1.SchemeGroupVersion.String()) + u.SetKind("CustomResourceDefinition") + unstructured.RemoveNestedField(u.Object, "metadata", "creationTimestamp") + unstructured.RemoveNestedField(u.Object, "status") + for k := range u.GetAnnotations() { + if strings.HasPrefix(k, "cloudwatch.aws.amazon.com/") { + // The other tool's manifest is upstream's, without our annotations. + ann := u.GetAnnotations() + delete(ann, k) + u.SetAnnotations(ann) + } + } + return u +} diff --git a/pkg/crdmanager/reconciler.go b/pkg/crdmanager/reconciler.go new file mode 100644 index 000000000..f9833a5ed --- /dev/null +++ b/pkg/crdmanager/reconciler.go @@ -0,0 +1,293 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package crdmanager installs the community ServiceMonitor and PodMonitor CRDs +// (monitoring.coreos.com/v1) when they are missing, so that ServiceMonitor and +// PodMonitor scraping works with no manual step. +// +// The rules (see the "Operator-managed ServiceMonitor and PodMonitor CRDs" design): +// - Never delete a CRD. +// - Create a CRD only when it does not exist, stamped with ownership metadata. +// - Change an existing CRD only when the ownership check proves the operator +// created it and nothing else changed it since. Phase 1 (this code) never +// changes an existing CRD; it only evaluates and reports the ownership check. +// - Safe with more than one operator pod (for example during a rolling +// upgrade): only a missing CRD is created, and AlreadyExists counts as success. +package crdmanager + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "sync" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Ownership metadata. These keys are a contract with customers and with later +// operator versions; do not rename them. +const ( + // FieldManager is sent on every write and recorded in metadata.managedFields. + FieldManager = "amazon-cloudwatch-agent-operator" + // ManagedByLabel marks a CRD the operator created. Its value is FieldManager. + ManagedByLabel = "cloudwatch.aws.amazon.com/crd-managed-by" + // AppliedVersionAnnotation is the upstream prometheus-operator version of the + // schema the operator last wrote. + AppliedVersionAnnotation = "cloudwatch.aws.amazon.com/crd-applied-version" + // AppliedGenerationAnnotation is the metadata.generation the CRD has after the + // operator's last write. A spec change by any other client moves + // metadata.generation past it. + AppliedGenerationAnnotation = "cloudwatch.aws.amazon.com/crd-applied-generation" + // UpstreamVersionAnnotation is set by prometheus-operator in its CRD manifests + // and identifies the live schema version, whoever wrote it. + UpstreamVersionAnnotation = "operator.prometheus.io/version" + + // Markers other install tools leave on an object they manage. Their presence + // means another tool now manages the CRD, even when it wrote content + // identical to ours and so moved no spec field ownership. + lastAppliedAnnotation = "kubectl.kubernetes.io/last-applied-configuration" // client-side kubectl apply, Argo CD default sync + helmReleaseAnnotation = "meta.helm.sh/release-name" // Helm, including --take-ownership + appManagedByLabel = "app.kubernetes.io/managed-by" // Helm and other tools + + controllerName = "prometheus-crd" + + // EventReasonNotManaged is emitted when a CRD exists that the operator did + // not create (or that something else changed), so the operator leaves it alone. + EventReasonNotManaged = "PrometheusCRDNotManaged" + // EventReasonCreated is emitted when the operator creates a missing CRD. + EventReasonCreated = "PrometheusCRDCreated" +) + +var crdGVK = apiextensionsv1.SchemeGroupVersion.WithKind("CustomResourceDefinition") + +// +kubebuilder:rbac:groups=apiextensions.k8s.io,resources=customresourcedefinitions,verbs=get;list;watch;create + +// Reconciler creates the embedded ServiceMonitor and PodMonitor CRDs when they +// are missing and reports, without writing, on CRDs it does not own. +type Reconciler struct { + Client client.Client + Log logr.Logger + Recorder record.EventRecorder + + desired map[string]*apiextensionsv1.CustomResourceDefinition + + mu sync.Mutex + // reported holds, per CRD name, the reason last logged and emitted as an + // Event, so a CRD the operator leaves alone is reported once rather than on + // every reconcile. + reported map[string]string +} + +// NewReconciler returns a Reconciler for the embedded CRDs. +func NewReconciler(c client.Client, log logr.Logger, recorder record.EventRecorder) (*Reconciler, error) { + crds, err := bundledCRDs() + if err != nil { + return nil, err + } + desired := make(map[string]*apiextensionsv1.CustomResourceDefinition, len(crds)) + for _, crd := range crds { + desired[crd.Name] = crd + } + return &Reconciler{ + Client: c, + Log: log, + Recorder: recorder, + desired: desired, + reported: map[string]string{}, + }, nil +} + +// SetupWithManager registers a controller that watches CRD metadata (not full +// schemas, so memory stays flat on clusters with many large CRDs), filtered to +// the managed names, and enqueues both names at start so a missing CRD is +// created even though no watch event exists for it. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + names := make(map[string]struct{}, len(r.desired)) + for name := range r.desired { + names[name] = struct{}{} + } + isManagedName := predicate.NewPredicateFuncs(func(o client.Object) bool { + _, ok := names[o.GetName()] + return ok + }) + startup := source.Func(func(_ context.Context, q workqueue.TypedRateLimitingInterface[reconcile.Request]) error { + for name := range names { + q.Add(reconcile.Request{NamespacedName: types.NamespacedName{Name: name}}) + } + return nil + }) + return ctrl.NewControllerManagedBy(mgr). + Named(controllerName). + For(&apiextensionsv1.CustomResourceDefinition{}, builder.OnlyMetadata, builder.WithPredicates(isManagedName)). + WatchesRawSource(startup). + Complete(r) +} + +// Reconcile creates the named CRD if it is missing. If it exists, Phase 1 does +// not write to it; it only reports when the ownership check fails. +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + desired, ok := r.desired[req.Name] + if !ok { + return reconcile.Result{}, nil + } + log := r.Log.WithValues("crd", req.Name) + + live := &metav1.PartialObjectMetadata{} + live.SetGroupVersionKind(crdGVK) + err := r.Client.Get(ctx, client.ObjectKey{Name: req.Name}, live) + switch { + case apierrors.IsNotFound(err): + return reconcile.Result{}, r.create(ctx, log, desired) + case err != nil: + return reconcile.Result{}, fmt.Errorf("get CRD %s: %w", req.Name, err) + } + + if live.GetDeletionTimestamp() != nil { + // Being deleted. The delete event reconciles again once it is gone. + log.V(1).Info("CRD is being deleted; waiting for removal before recreating") + return reconcile.Result{}, nil + } + + if owned, reason := IsOwned(live); !owned { + r.reportOnce(log, live, reason) + return reconcile.Result{}, nil + } + r.clearReported(req.Name) + // Phase 1: the CRD is ours. Upgrading an owned CRD to a newer embedded + // schema is Phase 2 and is intentionally not done here. + log.V(1).Info("CRD is managed by the operator; no change", "appliedVersion", live.GetAnnotations()[AppliedVersionAnnotation]) + return reconcile.Result{}, nil +} + +func (r *Reconciler) create(ctx context.Context, log logr.Logger, desired *apiextensionsv1.CustomResourceDefinition) error { + obj := desiredWithOwnership(desired) + err := r.Client.Create(ctx, obj, client.FieldOwner(FieldManager)) + switch { + case apierrors.IsAlreadyExists(err): + // Another client (or another operator pod during a rollout) created it + // between the read and the write. The watch reconciles the new object. + log.Info("CRD appeared before create completed; leaving it to the next reconcile") + return nil + case err != nil: + return fmt.Errorf("create CRD %s: %w", desired.Name, err) + } + log.Info("created CRD", "version", obj.Annotations[AppliedVersionAnnotation]) + r.clearReported(desired.Name) + r.Recorder.Eventf(obj, corev1.EventTypeNormal, EventReasonCreated, + "Created %s at prometheus-operator version %s", desired.Name, obj.Annotations[AppliedVersionAnnotation]) + return nil +} + +// desiredWithOwnership returns a copy of the embedded CRD with the ownership +// label and annotations set. A create always yields metadata.generation 1, so +// the applied generation is written in the create request itself. +func desiredWithOwnership(desired *apiextensionsv1.CustomResourceDefinition) *apiextensionsv1.CustomResourceDefinition { + obj := desired.DeepCopy() + obj.ResourceVersion = "" + if obj.Labels == nil { + obj.Labels = map[string]string{} + } + obj.Labels[ManagedByLabel] = FieldManager + if obj.Annotations == nil { + obj.Annotations = map[string]string{} + } + obj.Annotations[AppliedVersionAnnotation] = obj.Annotations[UpstreamVersionAnnotation] + obj.Annotations[AppliedGenerationAnnotation] = "1" + return obj +} + +// IsOwned reports whether the operator may treat the CRD as its own: it created +// the CRD and nothing else has changed it since the operator's last write. When +// it returns false, reason says which check failed. The checks, in order: +// 1. the crd-managed-by label is the operator's; +// 2. metadata.generation equals crd-applied-generation (no spec change since); +// 3. operator.prometheus.io/version equals crd-applied-version; +// 4. every managedFields entry that owns spec fields is the operator's; +// 5. no marker another install tool leaves is present. +func IsOwned(m metav1.Object) (bool, string) { + labels := m.GetLabels() + if labels[ManagedByLabel] != FieldManager { + return false, fmt.Sprintf("label %s=%s is not set", ManagedByLabel, FieldManager) + } + ann := m.GetAnnotations() + if got, want := ann[AppliedGenerationAnnotation], strconv.FormatInt(m.GetGeneration(), 10); got != want { + return false, fmt.Sprintf("metadata.generation %s does not match %s=%q", want, AppliedGenerationAnnotation, got) + } + applied, upstream := ann[AppliedVersionAnnotation], ann[UpstreamVersionAnnotation] + if applied == "" || applied != upstream { + return false, fmt.Sprintf("%s=%q does not match %s=%q", UpstreamVersionAnnotation, upstream, AppliedVersionAnnotation, applied) + } + mfs := m.GetManagedFields() + if len(mfs) == 0 { + return false, "metadata.managedFields is empty" + } + for _, mf := range mfs { + if mf.Manager == FieldManager || !ownsSpec(mf) { + continue + } + return false, fmt.Sprintf("field manager %q also owns fields under spec", mf.Manager) + } + // A client-side apply or Helm writing content identical to ours changes no + // spec field and no generation, so the checks above pass. The markers those + // tools leave show that they now manage the CRD. + for _, key := range []string{lastAppliedAnnotation, helmReleaseAnnotation} { + if _, ok := ann[key]; ok { + return false, fmt.Sprintf("annotation %s is set, so another tool manages this CRD", key) + } + } + if _, ok := labels[appManagedByLabel]; ok { + return false, fmt.Sprintf("label %s is set, so another tool manages this CRD", appManagedByLabel) + } + return true, "" +} + +// ownsSpec reports whether a managedFields entry owns any field under spec. +func ownsSpec(mf metav1.ManagedFieldsEntry) bool { + if mf.FieldsV1 == nil { + return false + } + raw := mf.FieldsV1.GetRawBytes() + if len(raw) == 0 { + return false + } + var fields map[string]json.RawMessage + if err := json.Unmarshal(raw, &fields); err != nil { + // Unreadable entry: assume it may own spec, so the operator steps back. + return true + } + _, ok := fields["f:spec"] + return ok +} + +func (r *Reconciler) reportOnce(log logr.Logger, live *metav1.PartialObjectMetadata, reason string) { + r.mu.Lock() + already := r.reported[live.Name] == reason + r.reported[live.Name] = reason + r.mu.Unlock() + if already { + return + } + log.Info("CRD exists and is not managed by the operator; leaving it unchanged", "reason", reason) + r.Recorder.Eventf(live, corev1.EventTypeNormal, EventReasonNotManaged, + "Leaving %s unchanged: %s", live.Name, reason) +} + +func (r *Reconciler) clearReported(name string) { + r.mu.Lock() + delete(r.reported, name) + r.mu.Unlock() +} diff --git a/pkg/crdmanager/reconciler_test.go b/pkg/crdmanager/reconciler_test.go new file mode 100644 index 000000000..47606ab70 --- /dev/null +++ b/pkg/crdmanager/reconciler_test.go @@ -0,0 +1,371 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package crdmanager + +import ( + "context" + "os" + "regexp" + "strings" + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ( + smName = "servicemonitors.monitoring.coreos.com" + pmName = "podmonitors.monitoring.coreos.com" +) + +func specFields() *metav1.FieldsV1 { + return metav1.NewFieldsV1(`{"f:metadata":{"f:labels":{}},"f:spec":{"f:group":{}}}`) +} + +func statusFields() *metav1.FieldsV1 { + return metav1.NewFieldsV1(`{"f:status":{"f:conditions":{}}}`) +} + +// ownedMeta returns metadata that passes every ownership check. +func ownedMeta() *metav1.ObjectMeta { + return &metav1.ObjectMeta{ + Name: smName, + Generation: 1, + Labels: map[string]string{ManagedByLabel: FieldManager}, + Annotations: map[string]string{ + UpstreamVersionAnnotation: BundledVersion, + AppliedVersionAnnotation: BundledVersion, + AppliedGenerationAnnotation: "1", + }, + ManagedFields: []metav1.ManagedFieldsEntry{ + {Manager: FieldManager, Operation: metav1.ManagedFieldsOperationUpdate, FieldsV1: specFields()}, + {Manager: "kube-apiserver", Operation: metav1.ManagedFieldsOperationUpdate, Subresource: "status", FieldsV1: statusFields()}, + }, + } +} + +func TestIsOwned(t *testing.T) { + tests := []struct { + name string + mutate func(m *metav1.ObjectMeta) + wantOwned bool + wantReason string + }{ + {name: "all checks pass", mutate: func(*metav1.ObjectMeta) {}, wantOwned: true}, + { + name: "label missing (installed by another tool)", + mutate: func(m *metav1.ObjectMeta) { m.Labels = nil }, + wantReason: "label", + }, + { + name: "label has another value", + mutate: func(m *metav1.ObjectMeta) { m.Labels[ManagedByLabel] = "helm" }, + wantReason: "label", + }, + { + name: "generation moved past the applied generation (spec changed by another client)", + mutate: func(m *metav1.ObjectMeta) { m.Generation = 2 }, + wantReason: "metadata.generation", + }, + { + name: "applied generation annotation missing", + mutate: func(m *metav1.ObjectMeta) { delete(m.Annotations, AppliedGenerationAnnotation) }, + wantReason: "metadata.generation", + }, + { + name: "upstream version differs from applied version (newer bundle applied over ours)", + mutate: func(m *metav1.ObjectMeta) { m.Annotations[UpstreamVersionAnnotation] = "0.93.0" }, + wantReason: UpstreamVersionAnnotation, + }, + { + name: "applied version annotation missing", + mutate: func(m *metav1.ObjectMeta) { + delete(m.Annotations, AppliedVersionAnnotation) + }, + wantReason: UpstreamVersionAnnotation, + }, + { + name: "managedFields empty", + mutate: func(m *metav1.ObjectMeta) { m.ManagedFields = nil }, + wantReason: "managedFields is empty", + }, + { + name: "second manager owns spec (server-side apply by another client)", + mutate: func(m *metav1.ObjectMeta) { + m.ManagedFields = append(m.ManagedFields, metav1.ManagedFieldsEntry{ + Manager: "kubectl", Operation: metav1.ManagedFieldsOperationApply, FieldsV1: specFields(), + }) + }, + wantReason: `"kubectl"`, + }, + { + name: "recreated from a saved copy of ours by another client", + mutate: func(m *metav1.ObjectMeta) { + m.ManagedFields[0].Manager = "kubectl-create" + }, + wantReason: `"kubectl-create"`, + }, + { + name: "another manager touching only metadata does not fail the check", + mutate: func(m *metav1.ObjectMeta) { + m.ManagedFields = append(m.ManagedFields, metav1.ManagedFieldsEntry{ + Manager: "kubectl-label", Operation: metav1.ManagedFieldsOperationUpdate, + FieldsV1: metav1.NewFieldsV1(`{"f:metadata":{"f:labels":{"f:team":{}}}}`), + }) + }, + wantOwned: true, + }, + { + name: "unreadable managedFields entry is treated as owning spec", + mutate: func(m *metav1.ObjectMeta) { + m.ManagedFields = append(m.ManagedFields, metav1.ManagedFieldsEntry{ + Manager: "other", FieldsV1: metav1.NewFieldsV1(`not-json`), + }) + }, + wantReason: `"other"`, + }, + { + name: "client-side kubectl apply marker (identical content, no spec manager)", + mutate: func(m *metav1.ObjectMeta) { m.Annotations[lastAppliedAnnotation] = "{}" }, + wantReason: lastAppliedAnnotation, + }, + { + name: "Helm release marker (helm --take-ownership)", + mutate: func(m *metav1.ObjectMeta) { m.Annotations[helmReleaseAnnotation] = "kps" }, + wantReason: helmReleaseAnnotation, + }, + { + name: "app.kubernetes.io/managed-by label from another tool", + mutate: func(m *metav1.ObjectMeta) { m.Labels[appManagedByLabel] = "Helm" }, + wantReason: appManagedByLabel, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := ownedMeta() + tt.mutate(m) + owned, reason := IsOwned(m) + assert.Equal(t, tt.wantOwned, owned, reason) + if !tt.wantOwned { + assert.Contains(t, reason, tt.wantReason) + } + }) + } +} + +func TestBundledCRDs(t *testing.T) { + crds, err := bundledCRDs() + require.NoError(t, err) + names := map[string]bool{} + for _, crd := range crds { + names[crd.Name] = true + assert.Equal(t, "monitoring.coreos.com", crd.Spec.Group) + assert.Equal(t, BundledVersion, crd.Annotations[UpstreamVersionAnnotation], + "embedded %s is not at BundledVersion", crd.Name) + require.Len(t, crd.Spec.Versions, 1) + assert.Equal(t, "v1", crd.Spec.Versions[0].Name) + } + assert.Equal(t, map[string]bool{smName: true, pmName: true}, names) +} + +// The embedded schema must match the prometheus-operator client the operator +// and target allocator are built with, so the operator never installs a schema +// the target allocator cannot read. +func TestBundledVersionMatchesGoMod(t *testing.T) { + raw, err := os.ReadFile("../../go.mod") + require.NoError(t, err) + m := regexp.MustCompile(`(?m)^\s*github\.com/prometheus-operator/prometheus-operator v(\S+)`).FindSubmatch(raw) + require.NotNil(t, m, "prometheus-operator not found in go.mod") + assert.Equal(t, BundledVersion, string(m[1])) +} + +func TestDesiredWithOwnership(t *testing.T) { + crds, err := bundledCRDs() + require.NoError(t, err) + orig := crds[0] + got := desiredWithOwnership(orig) + + assert.Equal(t, FieldManager, got.Labels[ManagedByLabel]) + assert.Equal(t, BundledVersion, got.Annotations[AppliedVersionAnnotation]) + assert.Equal(t, "1", got.Annotations[AppliedGenerationAnnotation]) + assert.Equal(t, BundledVersion, got.Annotations[UpstreamVersionAnnotation]) + assert.Empty(t, got.OwnerReferences, "operator-created CRDs must have no owner reference") + // The embedded object is not modified. + assert.NotContains(t, orig.Annotations, AppliedVersionAnnotation) + assert.NotContains(t, orig.Labels, ManagedByLabel) +} + +func newTestReconciler(t *testing.T, c client.Client) (*Reconciler, *record.FakeRecorder) { + t.Helper() + rec := record.NewFakeRecorder(10) + r, err := NewReconciler(c, logr.Discard(), rec) + require.NoError(t, err) + return r, rec +} + +func testScheme(t *testing.T) *runtime.Scheme { + t.Helper() + s := runtime.NewScheme() + require.NoError(t, apiextensionsv1.AddToScheme(s)) + return s +} + +func reconcileName(t *testing.T, r *Reconciler, name string) { + t.Helper() + _, err := r.Reconcile(context.Background(), reconcile.Request{NamespacedName: types.NamespacedName{Name: name}}) + require.NoError(t, err) +} + +func drain(rec *record.FakeRecorder) []string { + var out []string + for { + select { + case e := <-rec.Events: + out = append(out, e) + default: + return out + } + } +} + +func TestReconcileCreatesMissingCRDs(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(testScheme(t)).Build() + r, rec := newTestReconciler(t, c) + + for _, name := range []string{smName, pmName} { + reconcileName(t, r, name) + + got := &apiextensionsv1.CustomResourceDefinition{} + require.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: name}, got)) + assert.Equal(t, FieldManager, got.Labels[ManagedByLabel]) + assert.Equal(t, BundledVersion, got.Annotations[AppliedVersionAnnotation]) + assert.Equal(t, "1", got.Annotations[AppliedGenerationAnnotation]) + assert.Empty(t, got.OwnerReferences) + } + events := drain(rec) + require.Len(t, events, 2) + for _, e := range events { + assert.Contains(t, e, EventReasonCreated) + } +} + +func TestReconcileLeavesUnmanagedCRDUnchanged(t *testing.T) { + foreign := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: smName, + Annotations: map[string]string{UpstreamVersionAnnotation: "0.95.0"}, + }, + } + writes := 0 + c := fake.NewClientBuilder().WithScheme(testScheme(t)).WithObjects(foreign). + WithInterceptorFuncs(interceptor.Funcs{ + Create: func(ctx context.Context, cl client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + writes++ + return cl.Create(ctx, obj, opts...) + }, + Update: func(ctx context.Context, cl client.WithWatch, obj client.Object, opts ...client.UpdateOption) error { + writes++ + return cl.Update(ctx, obj, opts...) + }, + Patch: func(ctx context.Context, cl client.WithWatch, obj client.Object, p client.Patch, opts ...client.PatchOption) error { + writes++ + return cl.Patch(ctx, obj, p, opts...) + }, + Delete: func(ctx context.Context, cl client.WithWatch, obj client.Object, opts ...client.DeleteOption) error { + writes++ + return cl.Delete(ctx, obj, opts...) + }, + }).Build() + r, rec := newTestReconciler(t, c) + + // Reconcile several times: no write ever, and the step-back is reported once. + for i := 0; i < 3; i++ { + reconcileName(t, r, smName) + } + assert.Zero(t, writes, "operator must not write to a CRD it does not own") + + got := &apiextensionsv1.CustomResourceDefinition{} + require.NoError(t, c.Get(context.Background(), client.ObjectKey{Name: smName}, got)) + assert.Equal(t, "0.95.0", got.Annotations[UpstreamVersionAnnotation], "no downgrade") + assert.NotContains(t, got.Labels, ManagedByLabel) + + events := drain(rec) + require.Len(t, events, 1, "step-back is reported once, not per reconcile") + assert.Contains(t, events[0], EventReasonNotManaged) +} + +func TestReconcileAlreadyExistsOnCreateIsNotAnError(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(testScheme(t)). + WithInterceptorFuncs(interceptor.Funcs{ + Create: func(context.Context, client.WithWatch, client.Object, ...client.CreateOption) error { + return apierrors.NewAlreadyExists(apiextensionsv1.Resource("customresourcedefinitions"), smName) + }, + }).Build() + r, rec := newTestReconciler(t, c) + reconcileName(t, r, smName) + assert.Empty(t, drain(rec)) +} + +func TestReconcileCreateSendsFieldManager(t *testing.T) { + var owner string + c := fake.NewClientBuilder().WithScheme(testScheme(t)). + WithInterceptorFuncs(interceptor.Funcs{ + Create: func(ctx context.Context, cl client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + co := &client.CreateOptions{} + co.ApplyOptions(opts) + owner = co.FieldManager + return cl.Create(ctx, obj, opts...) + }, + }).Build() + r, _ := newTestReconciler(t, c) + reconcileName(t, r, pmName) + assert.Equal(t, FieldManager, owner) +} + +func TestReconcileIgnoresOtherCRDs(t *testing.T) { + c := fake.NewClientBuilder().WithScheme(testScheme(t)).Build() + r, rec := newTestReconciler(t, c) + reconcileName(t, r, "prometheuses.monitoring.coreos.com") + list := &apiextensionsv1.CustomResourceDefinitionList{} + require.NoError(t, c.List(context.Background(), list)) + assert.Empty(t, list.Items) + assert.Empty(t, drain(rec)) +} + +func TestReconcileSkipsCRDBeingDeleted(t *testing.T) { + now := metav1.Now() + deleting := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: smName, DeletionTimestamp: &now, Finalizers: []string{"customresourcecleanup.apiextensions.k8s.io"}}, + } + c := fake.NewClientBuilder().WithScheme(testScheme(t)).WithObjects(deleting).Build() + r, rec := newTestReconciler(t, c) + reconcileName(t, r, smName) + assert.Empty(t, drain(rec), "no report while the CRD is terminating") +} + +// The operator never asks for the delete verb on CRDs. +func TestRBACMarkerHasNoDelete(t *testing.T) { + raw, err := os.ReadFile("reconciler.go") + require.NoError(t, err) + for _, line := range strings.Split(string(raw), "\n") { + if strings.Contains(line, "+kubebuilder:rbac:groups=apiextensions.k8s.io") { + assert.NotContains(t, line, "delete") + assert.NotContains(t, line, "update") + assert.NotContains(t, line, "patch") + return + } + } + t.Fatal("apiextensions RBAC marker not found") +}