diff --git a/api/operator/v1beta1/vmagent_types.go b/api/operator/v1beta1/vmagent_types.go index 352bd1a17..27c7fbaf2 100644 --- a/api/operator/v1beta1/vmagent_types.go +++ b/api/operator/v1beta1/vmagent_types.go @@ -181,6 +181,12 @@ type VMAgentSpec struct { // Works in combination with NamespaceSelector. // +optional ScrapeConfigSelector *metav1.LabelSelector `json:"scrapeConfigSelector,omitempty"` + // ScrapeClasses defines the list of scrape classes to expose to scraping objects such as + // PodScrapes, ServiceScrapes, Probes and ScrapeConfigs. + // +listType=map + // +listMapKey=name + // +optional + ScrapeClasses []ScrapeClass `json:"scrapeClasses,omitempty"` // ScrapeConfigNamespaceSelector defines Namespaces to be selected for VMScrapeConfig discovery. // Works in combination with Selector. // NamespaceSelector nil - only objects at VMAgent namespace. @@ -383,6 +389,34 @@ func (cr *VMAgent) Validate() error { return fmt.Errorf("enableKubernetesAPISelectors cannot be used with daemonSetMode") } } + scrapeClassNames := make(map[string]struct{}) + defaultScrapeClass := false + for _, sc := range cr.Spec.ScrapeClasses { + if _, ok := scrapeClassNames[sc.Name]; ok { + return fmt.Errorf("duplicate scrape class name %q", sc.Name) + } + if ptr.Deref(sc.Default, false) { + if defaultScrapeClass { + return fmt.Errorf("multiple default scrape classes defined") + } + defaultScrapeClass = true + } + if sc.TLSConfig != nil { + if err := sc.TLSConfig.Validate(); err != nil { + return fmt.Errorf("bad tlsConfig for scrape class %q: %w", sc.Name, err) + } + } + if len(sc.Relabelings) > 0 { + if err := checkRelabelConfigs(sc.Relabelings); err != nil { + return fmt.Errorf("bad relabelings for scrape class %q: %w", sc.Name, err) + } + } + if len(sc.MetricRelabelings) > 0 { + if err := checkRelabelConfigs(sc.MetricRelabelings); err != nil { + return fmt.Errorf("bad metricRelabelings for scrape class %q: %w", sc.Name, err) + } + } + } return nil } @@ -446,6 +480,50 @@ type VMAgentRemoteWriteSettings struct { UseMultiTenantMode bool `json:"useMultiTenantMode,omitempty"` } +type ScrapeClass struct { + // name of the scrape class. + // + // +kubebuilder:validation:MinLength=1 + // +required + Name string `json:"name"` + + // default defines that the scrape applies to all scrape objects that + // don't configure an explicit scrape class name. + // + // Only one scrape class can be set as the default. + // + // +optional + Default *bool `json:"default,omitempty"` + + // tlsConfig defines the TLS settings to use for the scrape. When the + // scrape objects define their own CA, certificate and/or key, they take + // precedence over the corresponding scrape class fields. + // + // For now only the `caFile`, `certFile` and `keyFile` fields are supported. + // + // +optional + TLSConfig *TLSConfig `json:"tlsConfig,omitempty"` + + // authorization section for the ScrapeClass. + // It will only apply if the scrape resource doesn't specify any Authorization. + // +optional + Authorization *Authorization `json:"authorization,omitempty"` + + // Relabelings defines the relabeling rules to apply to all scrape targets. + // +optional + Relabelings []*RelabelConfig `json:"relabelings,omitempty"` + + // MetricRelabelings defines the relabeling rules to apply to all samples before ingestion. + // +optional + MetricRelabelings []*RelabelConfig `json:"metricRelabelings,omitempty"` + + // AttachMetadata defines additional metadata to the discovered targets. + // When the scrape object defines its own configuration, it takes + // precedence over the scrape class configuration. + // +optional + AttachMetadata *AttachMetadata `json:"attachMetadata,omitempty"` +} + // AWS defines AWS cloud auth specific params type AWS struct { // EC2Endpoint is an optional AWS EC2 API endpoint to use for the corresponding -remoteWrite.url if -remoteWrite.aws.useSigv4 is set diff --git a/api/operator/v1beta1/vmservicescrape_types.go b/api/operator/v1beta1/vmservicescrape_types.go index 55be1a64b..889e7b4c9 100644 --- a/api/operator/v1beta1/vmservicescrape_types.go +++ b/api/operator/v1beta1/vmservicescrape_types.go @@ -52,6 +52,9 @@ type VMServiceScrapeSpec struct { // AttachMetadata configures metadata attaching from service discovery // +optional AttachMetadata AttachMetadata `json:"attach_metadata,omitempty"` + // ScrapeClass defined scrape class to apply + // +optional + ScrapeClassName *string `json:"scrapeClass,omitempty"` } // UnmarshalJSON implements json.Unmarshaler interface diff --git a/api/operator/v1beta1/zz_generated.deepcopy.go b/api/operator/v1beta1/zz_generated.deepcopy.go index f394bbe6d..a946ebc7f 100644 --- a/api/operator/v1beta1/zz_generated.deepcopy.go +++ b/api/operator/v1beta1/zz_generated.deepcopy.go @@ -2722,6 +2722,63 @@ func (in *RuleGroup) DeepCopy() *RuleGroup { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ScrapeClass) DeepCopyInto(out *ScrapeClass) { + *out = *in + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(bool) + **out = **in + } + if in.TLSConfig != nil { + in, out := &in.TLSConfig, &out.TLSConfig + *out = new(TLSConfig) + (*in).DeepCopyInto(*out) + } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(Authorization) + (*in).DeepCopyInto(*out) + } + if in.Relabelings != nil { + in, out := &in.Relabelings, &out.Relabelings + *out = make([]*RelabelConfig, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RelabelConfig) + (*in).DeepCopyInto(*out) + } + } + } + if in.MetricRelabelings != nil { + in, out := &in.MetricRelabelings, &out.MetricRelabelings + *out = make([]*RelabelConfig, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(RelabelConfig) + (*in).DeepCopyInto(*out) + } + } + } + if in.AttachMetadata != nil { + in, out := &in.AttachMetadata, &out.AttachMetadata + *out = new(AttachMetadata) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ScrapeClass. +func (in *ScrapeClass) DeepCopy() *ScrapeClass { + if in == nil { + return nil + } + out := new(ScrapeClass) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ScrapeObjectStatus) DeepCopyInto(out *ScrapeObjectStatus) { *out = *in @@ -4093,6 +4150,13 @@ func (in *VMAgentSpec) DeepCopyInto(out *VMAgentSpec) { *out = new(metav1.LabelSelector) (*in).DeepCopyInto(*out) } + if in.ScrapeClasses != nil { + in, out := &in.ScrapeClasses, &out.ScrapeClasses + *out = make([]ScrapeClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.ScrapeConfigNamespaceSelector != nil { in, out := &in.ScrapeConfigNamespaceSelector, &out.ScrapeConfigNamespaceSelector *out = new(metav1.LabelSelector) @@ -6289,6 +6353,11 @@ func (in *VMServiceScrapeSpec) DeepCopyInto(out *VMServiceScrapeSpec) { in.Selector.DeepCopyInto(&out.Selector) in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) in.AttachMetadata.DeepCopyInto(&out.AttachMetadata) + if in.ScrapeClassName != nil { + in, out := &in.ScrapeClassName, &out.ScrapeClassName + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VMServiceScrapeSpec. diff --git a/config/crd/overlay/crd.yaml b/config/crd/overlay/crd.yaml index 94d0fb4be..374184438 100644 --- a/config/crd/overlay/crd.yaml +++ b/config/crd/overlay/crd.yaml @@ -9710,6 +9710,390 @@ spec: schedulerName: description: SchedulerName - defines kubernetes scheduler name type: string + scrapeClasses: + description: |- + ScrapeClasses defines the list of scrape classes to expose to scraping objects such as + PodScrapes, ServiceScrapes, Probes and ScrapeConfigs. + items: + properties: + attachMetadata: + description: |- + AttachMetadata defines additional metadata to the discovered targets. + When the scrape object defines its own configuration, it takes + precedence over the scrape class configuration. + properties: + node: + description: |- + Node instructs vmagent to add node specific metadata from service discovery + Valid for roles: pod, endpoints, endpointslice. + type: boolean + type: object + authorization: + description: |- + authorization section for the ScrapeClass. + It will only apply if the scrape resource doesn't specify any Authorization. + properties: + credentials: + description: Reference to the secret with value for authorization + 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 + credentialsFile: + description: File with value for authorization + type: string + type: + description: Type of authorization, default to bearer + type: string + type: object + default: + description: |- + default defines that the scrape applies to all scrape objects that + don't configure an explicit scrape class name. + + Only one scrape class can be set as the default. + type: boolean + metricRelabelings: + description: MetricRelabelings defines the relabeling rules + to apply to all samples before ingestion. + items: + description: |- + RelabelConfig allows dynamic rewriting of the label set + More info: https://docs.victoriametrics.com/victoriametrics/#relabeling + properties: + action: + description: Action to perform based on regex matching. + Default is 'replace' + type: string + if: + description: 'If represents metricsQL match expression + (or list of expressions): ''{__name__=~"foo_.*"}''' + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + description: 'Labels is used together with Match for `action: + graphite`' + type: object + match: + description: 'Match is used together with Labels for `action: + graphite`' + type: string + modulus: + description: Modulus to take of the hash of the source + label values. + format: int64 + type: integer + regex: + description: |- + Regular expression against which the extracted value is matched. Default is '(.*)' + victoriaMetrics supports multiline regex joined with | + https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling-enhancements + x-kubernetes-preserve-unknown-fields: true + replacement: + description: |- + Replacement value against which a regex replace is performed if the + regular expression matches. Regex capture groups are available. Default is '$1' + type: string + separator: + description: Separator placed between concatenated source + label values. default is ';'. + type: string + source_labels: + description: |- + UnderScoreSourceLabels - additional form of source labels source_labels + for compatibility with original relabel config. + if set both sourceLabels and source_labels, sourceLabels has priority. + for details https://github.com/VictoriaMetrics/operator/issues/131 + items: + type: string + type: array + sourceLabels: + description: |- + The source labels select values from existing labels. Their content is concatenated + using the configured separator and matched against the configured regular expression + for the replace, keep, and drop actions. + items: + type: string + type: array + target_label: + description: |- + UnderScoreTargetLabel - additional form of target label - target_label + for compatibility with original relabel config. + if set both targetLabel and target_label, targetLabel has priority. + for details https://github.com/VictoriaMetrics/operator/issues/131 + type: string + targetLabel: + description: |- + Label to which the resulting value is written in a replace action. + It is mandatory for replace actions. Regex capture groups are available. + type: string + type: object + type: array + name: + description: name of the scrape class. + minLength: 1 + type: string + relabelings: + description: Relabelings defines the relabeling rules to apply + to all scrape targets. + items: + description: |- + RelabelConfig allows dynamic rewriting of the label set + More info: https://docs.victoriametrics.com/victoriametrics/#relabeling + properties: + action: + description: Action to perform based on regex matching. + Default is 'replace' + type: string + if: + description: 'If represents metricsQL match expression + (or list of expressions): ''{__name__=~"foo_.*"}''' + x-kubernetes-preserve-unknown-fields: true + labels: + additionalProperties: + type: string + description: 'Labels is used together with Match for `action: + graphite`' + type: object + match: + description: 'Match is used together with Labels for `action: + graphite`' + type: string + modulus: + description: Modulus to take of the hash of the source + label values. + format: int64 + type: integer + regex: + description: |- + Regular expression against which the extracted value is matched. Default is '(.*)' + victoriaMetrics supports multiline regex joined with | + https://docs.victoriametrics.com/victoriametrics/vmagent/#relabeling-enhancements + x-kubernetes-preserve-unknown-fields: true + replacement: + description: |- + Replacement value against which a regex replace is performed if the + regular expression matches. Regex capture groups are available. Default is '$1' + type: string + separator: + description: Separator placed between concatenated source + label values. default is ';'. + type: string + source_labels: + description: |- + UnderScoreSourceLabels - additional form of source labels source_labels + for compatibility with original relabel config. + if set both sourceLabels and source_labels, sourceLabels has priority. + for details https://github.com/VictoriaMetrics/operator/issues/131 + items: + type: string + type: array + sourceLabels: + description: |- + The source labels select values from existing labels. Their content is concatenated + using the configured separator and matched against the configured regular expression + for the replace, keep, and drop actions. + items: + type: string + type: array + target_label: + description: |- + UnderScoreTargetLabel - additional form of target label - target_label + for compatibility with original relabel config. + if set both targetLabel and target_label, targetLabel has priority. + for details https://github.com/VictoriaMetrics/operator/issues/131 + type: string + targetLabel: + description: |- + Label to which the resulting value is written in a replace action. + It is mandatory for replace actions. Regex capture groups are available. + type: string + type: object + type: array + tlsConfig: + description: |- + tlsConfig defines the TLS settings to use for the scrape. When the + scrape objects define their own CA, certificate and/or key, they take + precedence over the corresponding scrape class fields. + + For now only the `caFile`, `certFile` and `keyFile` fields are supported. + properties: + ca: + description: Struct containing the CA cert to use for the + targets. + properties: + configMap: + description: 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 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: Path to the CA cert in the container to use + for the targets. + type: string + cert: + description: Struct containing the client cert file for + the targets. + properties: + configMap: + description: 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 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: Path to the client cert file in the container + for the targets. + type: string + insecureSkipVerify: + description: Disable target certificate validation. + type: boolean + keyFile: + description: Path to the client key file in the container + for the targets. + type: string + keySecret: + description: 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 + serverName: + description: Used to verify the hostname for the targets. + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map scrapeConfigNamespaceSelector: description: |- ScrapeConfigNamespaceSelector defines Namespaces to be selected for VMScrapeConfig discovery. @@ -36022,6 +36406,9 @@ spec: samples that will be accepted. format: int64 type: integer + scrapeClass: + description: ScrapeClass defined scrape class to apply + type: string selector: description: Selector to select Endpoints objects by corresponding Service labels. diff --git a/internal/controller/operator/converter/apis.go b/internal/controller/operator/converter/apis.go index c8571d4e6..8bee5d74a 100644 --- a/internal/controller/operator/converter/apis.go +++ b/internal/controller/operator/converter/apis.go @@ -118,6 +118,7 @@ func ConvertServiceMonitor(serviceMon *promv1.ServiceMonitor, conf *config.BaseO Any: serviceMon.Spec.NamespaceSelector.Any, MatchNames: serviceMon.Spec.NamespaceSelector.MatchNames, }, + ScrapeClassName: serviceMon.Spec.ScrapeClassName, }, } if serviceMon.Spec.SampleLimit != nil { diff --git a/internal/controller/operator/factory/vmagent/servicescrape.go b/internal/controller/operator/factory/vmagent/servicescrape.go index 355a22dfc..21c2735aa 100644 --- a/internal/controller/operator/factory/vmagent/servicescrape.go +++ b/internal/controller/operator/factory/vmagent/servicescrape.go @@ -20,6 +20,13 @@ func generateServiceScrapeConfig( ac *build.AssetsCache, se vmv1beta1.VMAgentSecurityEnforcements, ) (yaml.MapSlice, error) { + scrapeClass := getScrapeClassOrDefault(sc.Spec.ScrapeClassName, cr) + ep.Authorization = mergeAuthorizationWithScrapeClass(ep.Authorization, scrapeClass) + ep.AttachMetadata = mergeAttachMetadataWithScrapeClass(ep.AttachMetadata, scrapeClass) + ep.RelabelConfigs = mergeRelabelConfigsWithScrapeClass(ep.RelabelConfigs, scrapeClass) + ep.MetricRelabelConfigs = mergeMetricRelabelConfigsWithScrapeClass(ep.MetricRelabelConfigs, scrapeClass) + ep.TLSConfig = mergeTLSConfigWithScrapeClass(ep.TLSConfig, scrapeClass) + cfg := yaml.MapSlice{ { Key: "job_name", diff --git a/internal/controller/operator/factory/vmagent/servicescrape_test.go b/internal/controller/operator/factory/vmagent/servicescrape_test.go index af3d28556..9ed0f579c 100644 --- a/internal/controller/operator/factory/vmagent/servicescrape_test.go +++ b/internal/controller/operator/factory/vmagent/servicescrape_test.go @@ -1265,6 +1265,712 @@ relabel_configs: replacement: ${1} - target_label: endpoint replacement: "8080" +`, + }, + + { + name: "relabelings in scrapeClass and in endpoint", + args: args{ + cr: &vmv1beta1.VMAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-vmagent", + Namespace: "default", + }, + Spec: vmv1beta1.VMAgentSpec{ + ScrapeClasses: []vmv1beta1.ScrapeClass{ + { + Name: "default", + Default: ptr.To(true), + Relabelings: []*vmv1beta1.RelabelConfig{ + { + Action: "replace", + SourceLabels: []string{"__meta_kubernetes_pod_app_name"}, + TargetLabel: "app", + }, + }, + }, + { + Name: "not-default", + Relabelings: []*vmv1beta1.RelabelConfig{ + { + Action: "replace", + SourceLabels: []string{"__meta_kubernetes_pod_node_name"}, + TargetLabel: "node", + }, + }, + }, + }, + }, + }, + sc: &vmv1beta1.VMServiceScrape{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scrape", + Namespace: "default", + }, + Spec: vmv1beta1.VMServiceScrapeSpec{ + Endpoints: []vmv1beta1.Endpoint{ + { + Port: "8080", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + RelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + Action: "replace", + SourceLabels: []string{"__meta_kubernetes_namespace"}, + TargetLabel: "namespace", + }, + }, + }, + }, + }, + }, + }, + ep: vmv1beta1.Endpoint{ + AttachMetadata: vmv1beta1.AttachMetadata{ + Node: ptr.To(true), + }, + Port: "8080", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + RelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + Action: "replace", + SourceLabels: []string{"__meta_kubernetes_namespace"}, + TargetLabel: "namespace", + }, + }, + }, + }, + i: 0, + apiserverConfig: nil, + se: vmv1beta1.VMAgentSecurityEnforcements{ + OverrideHonorLabels: false, + OverrideHonorTimestamps: false, + IgnoreNamespaceSelectors: false, + EnforcedNamespaceLabel: "", + }, + }, + want: `job_name: serviceScrape/default/test-scrape/0 +kubernetes_sd_configs: +- role: endpoints + attach_metadata: + node: true + namespaces: + names: + - default +honor_labels: false +relabel_configs: +- action: keep + source_labels: + - __meta_kubernetes_endpoint_port_name + regex: "8080" +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Node;(.*) + replacement: ${1} + target_label: node +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Pod;(.*) + replacement: ${1} + target_label: pod +- source_labels: + - __meta_kubernetes_pod_name + target_label: pod +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container +- source_labels: + - __meta_kubernetes_namespace + target_label: namespace +- source_labels: + - __meta_kubernetes_service_name + target_label: service +- source_labels: + - __meta_kubernetes_service_name + target_label: job + replacement: ${1} +- target_label: endpoint + replacement: "8080" +- source_labels: + - __meta_kubernetes_namespace + target_label: namespace + action: replace +- source_labels: + - __meta_kubernetes_pod_app_name + target_label: app + action: replace +`, + }, + { + name: "scrapeClass with TLS and relabel config inheritance", + args: args{ + cr: &vmv1beta1.VMAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-vmagent", + Namespace: "default", + }, + Spec: vmv1beta1.VMAgentSpec{ + ScrapeClasses: []vmv1beta1.ScrapeClass{ + { + Name: "custom-class", + Default: ptr.To(false), + TLSConfig: &vmv1beta1.TLSConfig{ + CAFile: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + Cert: vmv1beta1.SecretOrConfigMap{ + Secret: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "tls-secret", + }, + Key: "cert", + }, + }, + }, + Relabelings: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"__meta_kubernetes_pod_node_name"}, + TargetLabel: "node", + Action: "replace", + }, + }, + }, + }, + }, + }, + sc: &vmv1beta1.VMServiceScrape{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scrape", + Namespace: "default", + }, + Spec: vmv1beta1.VMServiceScrapeSpec{ + ScrapeClassName: ptr.To("custom-class"), + Endpoints: []vmv1beta1.Endpoint{ + { + Port: "8080", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + RelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"__meta_kubernetes_pod_container_name"}, + TargetLabel: "container", + Action: "replace", + }, + }, + }, + }, + }, + }, + }, + ep: vmv1beta1.Endpoint{ + Port: "8080", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + RelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"__meta_kubernetes_pod_container_name"}, + TargetLabel: "container", + Action: "replace", + }, + }, + }, + }, + i: 0, + }, + predefinedObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + "cert": []byte("cert-value"), + }, + }, + }, + want: `job_name: serviceScrape/default/test-scrape/0 +kubernetes_sd_configs: +- role: endpoints + namespaces: + names: + - default +honor_labels: false +relabel_configs: +- action: keep + source_labels: + - __meta_kubernetes_endpoint_port_name + regex: "8080" +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Node;(.*) + replacement: ${1} + target_label: node +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Pod;(.*) + replacement: ${1} + target_label: pod +- source_labels: + - __meta_kubernetes_pod_name + target_label: pod +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container +- source_labels: + - __meta_kubernetes_namespace + target_label: namespace +- source_labels: + - __meta_kubernetes_service_name + target_label: service +- source_labels: + - __meta_kubernetes_service_name + target_label: job + replacement: ${1} +- target_label: endpoint + replacement: "8080" +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container + action: replace +- source_labels: + - __meta_kubernetes_pod_node_name + target_label: node + action: replace +tls_config: + ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + cert_file: /etc/vmagent-tls/certs/default_tls-secret_cert +`, + }, + { + name: "default scrapeClass with attachMetadata", + args: args{ + cr: &vmv1beta1.VMAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-vmagent", + Namespace: "default", + }, + Spec: vmv1beta1.VMAgentSpec{ + ScrapeClasses: []vmv1beta1.ScrapeClass{ + { + Name: "default", + Default: ptr.To(true), + AttachMetadata: &vmv1beta1.AttachMetadata{ + Node: ptr.To(true), + }, + }, + }, + }, + }, + sc: &vmv1beta1.VMServiceScrape{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scrape", + Namespace: "default", + }, + Spec: vmv1beta1.VMServiceScrapeSpec{ + Endpoints: []vmv1beta1.Endpoint{ + { + Port: "8080", + }, + }, + }, + }, + ep: vmv1beta1.Endpoint{ + Port: "8080", + }, + i: 0, + }, + want: `job_name: serviceScrape/default/test-scrape/0 +kubernetes_sd_configs: +- role: endpoints + attach_metadata: + node: true + namespaces: + names: + - default +honor_labels: false +relabel_configs: +- action: keep + source_labels: + - __meta_kubernetes_endpoint_port_name + regex: "8080" +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Node;(.*) + replacement: ${1} + target_label: node +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Pod;(.*) + replacement: ${1} + target_label: pod +- source_labels: + - __meta_kubernetes_pod_name + target_label: pod +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container +- source_labels: + - __meta_kubernetes_namespace + target_label: namespace +- source_labels: + - __meta_kubernetes_service_name + target_label: service +- source_labels: + - __meta_kubernetes_service_name + target_label: job + replacement: ${1} +- target_label: endpoint + replacement: "8080" +`, + }, + { + name: "scrapeClass with authorization and TLS config inheritance", + args: args{ + cr: &vmv1beta1.VMAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-vmagent", + Namespace: "default", + }, + Spec: vmv1beta1.VMAgentSpec{ + ScrapeClasses: []vmv1beta1.ScrapeClass{ + { + Name: "secure-class", + Default: ptr.To(false), + Authorization: &vmv1beta1.Authorization{ + Credentials: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "auth-secret", + }, + Key: "token", + }, + Type: "Bearer", + }, + TLSConfig: &vmv1beta1.TLSConfig{ + CAFile: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt", + InsecureSkipVerify: false, + }, + }, + }, + }, + }, + sc: &vmv1beta1.VMServiceScrape{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scrape", + Namespace: "default", + }, + Spec: vmv1beta1.VMServiceScrapeSpec{ + ScrapeClassName: ptr.To("secure-class"), + Endpoints: []vmv1beta1.Endpoint{ + { + Port: "8443", + }, + }, + }, + }, + ep: vmv1beta1.Endpoint{ + Port: "8443", + }, + i: 0, + }, + predefinedObjects: []runtime.Object{ + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "auth-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + "token": []byte("secret-token-value"), + }, + }, + }, + want: `job_name: serviceScrape/default/test-scrape/0 +kubernetes_sd_configs: +- role: endpoints + namespaces: + names: + - default +honor_labels: false +relabel_configs: +- action: keep + source_labels: + - __meta_kubernetes_endpoint_port_name + regex: "8443" +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Node;(.*) + replacement: ${1} + target_label: node +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Pod;(.*) + replacement: ${1} + target_label: pod +- source_labels: + - __meta_kubernetes_pod_name + target_label: pod +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container +- source_labels: + - __meta_kubernetes_namespace + target_label: namespace +- source_labels: + - __meta_kubernetes_service_name + target_label: service +- source_labels: + - __meta_kubernetes_service_name + target_label: job + replacement: ${1} +- target_label: endpoint + replacement: "8443" +tls_config: + ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt +authorization: + credentials: secret-token-value + type: Bearer +`, + }, + { + name: "scrapeClass with multiple metric relabelings merge", + args: args{ + cr: &vmv1beta1.VMAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default-vmagent", + Namespace: "default", + }, + Spec: vmv1beta1.VMAgentSpec{ + ScrapeClasses: []vmv1beta1.ScrapeClass{ + { + Name: "metrics-class", + Default: ptr.To(true), + MetricRelabelings: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"__name__"}, + Regex: vmv1beta1.StringOrArray{"go_.*"}, + Action: "keep", + }, + { + TargetLabel: "scrape_class", + Replacement: ptr.To("metrics-class"), + Action: "replace", + }, + }, + }, + }, + }, + }, + sc: &vmv1beta1.VMServiceScrape{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scrape", + Namespace: "default", + }, + Spec: vmv1beta1.VMServiceScrapeSpec{ + Endpoints: []vmv1beta1.Endpoint{ + { + Port: "9090", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + MetricRelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"instance"}, + TargetLabel: "endpoint_instance", + Action: "replace", + }, + }, + }, + }, + }, + }, + }, + ep: vmv1beta1.Endpoint{ + Port: "9090", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + MetricRelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"instance"}, + TargetLabel: "endpoint_instance", + Action: "replace", + }, + }, + }, + }, + i: 0, + }, + want: `job_name: serviceScrape/default/test-scrape/0 +kubernetes_sd_configs: +- role: endpoints + namespaces: + names: + - default +honor_labels: false +relabel_configs: +- action: keep + source_labels: + - __meta_kubernetes_endpoint_port_name + regex: "9090" +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Node;(.*) + replacement: ${1} + target_label: node +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Pod;(.*) + replacement: ${1} + target_label: pod +- source_labels: + - __meta_kubernetes_pod_name + target_label: pod +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container +- source_labels: + - __meta_kubernetes_namespace + target_label: namespace +- source_labels: + - __meta_kubernetes_service_name + target_label: service +- source_labels: + - __meta_kubernetes_service_name + target_label: job + replacement: ${1} +- target_label: endpoint + replacement: "9090" +metric_relabel_configs: +- source_labels: + - instance + target_label: endpoint_instance + action: replace +- source_labels: + - __name__ + regex: go_.* + action: keep +- target_label: scrape_class + replacement: metrics-class + action: replace +`, + }, + { + name: "no default scrapeclass and no scrapeclass specified", + args: args{ + cr: &vmv1beta1.VMAgent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "thedefault-vmagent", + Namespace: "default", + }, + Spec: vmv1beta1.VMAgentSpec{ + ScrapeClasses: []vmv1beta1.ScrapeClass{ + { + Name: "non-default-class", + Relabelings: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"__meta_kubernetes_pod_node_name"}, + TargetLabel: "node", + Action: "replace", + }, + }, + }, + }, + }, + }, + sc: &vmv1beta1.VMServiceScrape{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-scrape", + Namespace: "default", + }, + Spec: vmv1beta1.VMServiceScrapeSpec{ + Endpoints: []vmv1beta1.Endpoint{ + { + Port: "8080", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + RelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"__meta_kubernetes_pod_container_name"}, + TargetLabel: "container", + Action: "replace", + }, + }, + }, + }, + }, + }, + }, + ep: vmv1beta1.Endpoint{ + Port: "8080", + EndpointRelabelings: vmv1beta1.EndpointRelabelings{ + RelabelConfigs: []*vmv1beta1.RelabelConfig{ + { + SourceLabels: []string{"__meta_kubernetes_pod_container_name"}, + TargetLabel: "container", + Action: "replace", + }, + }, + }, + }, + i: 0, + }, + want: `job_name: serviceScrape/default/test-scrape/0 +kubernetes_sd_configs: +- role: endpoints + namespaces: + names: + - default +honor_labels: false +relabel_configs: +- action: keep + source_labels: + - __meta_kubernetes_endpoint_port_name + regex: "8080" +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Node;(.*) + replacement: ${1} + target_label: node +- source_labels: + - __meta_kubernetes_endpoint_address_target_kind + - __meta_kubernetes_endpoint_address_target_name + separator: ; + regex: Pod;(.*) + replacement: ${1} + target_label: pod +- source_labels: + - __meta_kubernetes_pod_name + target_label: pod +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container +- source_labels: + - __meta_kubernetes_namespace + target_label: namespace +- source_labels: + - __meta_kubernetes_service_name + target_label: service +- source_labels: + - __meta_kubernetes_service_name + target_label: job + replacement: ${1} +- target_label: endpoint + replacement: "8080" +- source_labels: + - __meta_kubernetes_pod_container_name + target_label: container + action: replace `, }, } diff --git a/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go b/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go index 958585a30..472fea4b3 100644 --- a/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go +++ b/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go @@ -47,6 +47,9 @@ func (so *scrapeObjects) validateObjects(cr *vmv1beta1.VMAgent) { } } } + if err := validateScrapeClassExists(ss.Spec.ScrapeClassName, cr); err != nil { + return err + } if err := ss.Validate(); err != nil { return err } @@ -1206,3 +1209,96 @@ func getAssetsCache(ctx context.Context, rclient client.Client, cr *vmv1beta1.VM } return build.NewAssetsCache(ctx, rclient, cfg) } +func validateScrapeClassExists(scrapeClassName *string, cr *vmv1beta1.VMAgent) error { + if scrapeClassName == nil { + return nil + } + for _, sc := range cr.Spec.ScrapeClasses { + if sc.Name == *scrapeClassName { + return nil + } + } + return fmt.Errorf("scrape class %q not found in VMAgent %s/%s", *scrapeClassName, cr.Namespace, cr.Name) +} +func mergeAuthorizationWithScrapeClass(authz *vmv1beta1.Authorization, scrapeClass vmv1beta1.ScrapeClass) *vmv1beta1.Authorization { + if authz == nil { + return scrapeClass.Authorization + } + if scrapeClass.Authorization == nil { + return authz + } + + if authz.Credentials == nil { + authz.Credentials = scrapeClass.Authorization.Credentials + } + + if authz.Credentials == nil && authz.CredentialsFile == "" { + authz.Credentials = scrapeClass.Authorization.Credentials + authz.CredentialsFile = scrapeClass.Authorization.CredentialsFile + } + + return authz +} +func mergeAttachMetadataWithScrapeClass(am vmv1beta1.AttachMetadata, scrapeClass vmv1beta1.ScrapeClass) vmv1beta1.AttachMetadata { + if scrapeClass.AttachMetadata == nil { + return am + } + + if am.Node == nil { + am.Node = scrapeClass.AttachMetadata.Node + } + + return am +} +func mergeRelabelConfigsWithScrapeClass(rcs []*vmv1beta1.RelabelConfig, scrapeClass vmv1beta1.ScrapeClass) []*vmv1beta1.RelabelConfig { + if len(scrapeClass.Relabelings) == 0 { + return rcs + } + return append(rcs, scrapeClass.Relabelings...) +} +func mergeMetricRelabelConfigsWithScrapeClass(mrcs []*vmv1beta1.RelabelConfig, scrapeClass vmv1beta1.ScrapeClass) []*vmv1beta1.RelabelConfig { + if len(scrapeClass.MetricRelabelings) == 0 { + return mrcs + } + return append(mrcs, scrapeClass.MetricRelabelings...) +} +func mergeTLSConfigWithScrapeClass(tlsConfig *vmv1beta1.TLSConfig, scrapeClass vmv1beta1.ScrapeClass) *vmv1beta1.TLSConfig { + if tlsConfig == nil { + return scrapeClass.TLSConfig + } + + if scrapeClass.TLSConfig == nil { + return tlsConfig + } + + if tlsConfig.CAFile == "" && tlsConfig.CA == (vmv1beta1.SecretOrConfigMap{}) { + tlsConfig.CAFile = scrapeClass.TLSConfig.CAFile + } + + if tlsConfig.CertFile == "" && tlsConfig.Cert == (vmv1beta1.SecretOrConfigMap{}) { + tlsConfig.CertFile = scrapeClass.TLSConfig.CertFile + } + + if tlsConfig.KeyFile == "" && tlsConfig.KeySecret == nil { + tlsConfig.KeyFile = scrapeClass.TLSConfig.KeyFile + } + + return tlsConfig +} + +func getScrapeClassOrDefault(name *string, vmagent *vmv1beta1.VMAgent) vmv1beta1.ScrapeClass { + if name != nil { + for _, scrapeClass := range vmagent.Spec.ScrapeClasses { + if scrapeClass.Name == *name { + return scrapeClass + } + } + } + + for _, scrapeClass := range vmagent.Spec.ScrapeClasses { + if ptr.Deref(scrapeClass.Default, false) { + return scrapeClass + } + } + return vmv1beta1.ScrapeClass{} +}