Repository: VictoriaMetrics/operator

Issue:
Support for scrapeclass in VMServiceScrape, VMPodScrape, VMProbe
**Use-case**

We use prometheus ServiceMonitor, PodMonitor, and Probe as inputs for scraping targets. These resources are translated by the VM operator into VMServiceScrape, VMPodScrape, and VMProbe, which are then scraped by VMagent.

Recently, the Prometheus Operator introduced [scrapeClass](https://prometheus-operator.dev/docs/developer/scrapeclass) to standardize common configurations such as relabeling rules, TLS certificates, and authentication. This feature enables strict mTLS support in PodMonitors and Probes (ServiceMonitor already supported it via tlsConfig) for scraping workloads inside an Istio mesh.

While `VMPodScrape` already supports mTLS through tlsConfig, the issue arises when scraping config comes from ServiceMonitor, PodMonitor, and Probe. When these CRs are converted into their VM equivalents, the TLS settings from scrapeClass are not passed through, leaving us unable to use mTLS with VMPodScrape and VMProbe.

**Steps to reproduce**
 - Enable the scrapeclass in prometheus helm chart like [this](https://prometheus-operator.dev/docs/developer/scrapeclass/#defining-a-scrapeclass-in-prometheus-resource)
 - Apply the podmonitor as below
 ```
kubectl apply -f - <<EOF
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: test-podmonitor
  labels:
    release: prometheus 
spec:
  scrapeClass: istio-mtls
  podMetricsEndpoints:
  - interval: 60s
    path: /metrics
    port: metrics-port
    relabelings:
    - action: replace
      sourceLabels: [__meta_kubernetes_pod_label_status]
      targetLabel: status
    - action: replace
      sourceLabels: [pod]
      targetLabel: instance
  selector:
    matchLabels:
      app.kubernetes.io/instance: test
EOF
```
- The pod monitor has been loaded with TLS config in prometheus
- The translated VMPodscrape has been loaded in the VMagent **but without TLS config**

**VM Versions:**
- Helm chart - victoria-metrics-k8s-stack-0.27.6
- VMOperator - v0.48.3
- VMAgent - v1.105.0
- App version - v1.105.0

Could you please help if there are plans to support scrapeClass in the VM operator? If not, do you have any recommendations on how to achieve strict mTLS support for VMPodScrape and VMProbe when using ServiceMonitor, PodMonitor, and Probe as inputs? Thanks in advance!

Hints from issue comments:
@endesapt: I would like to try to work on that feature. As for me the best way to implement this is to copy Prometheus Operator approach: add scrapeClass field to PodScrape, ServiceScrape, SCrapeConfig, Probe . Add scrapeClasses field to VMAgent similar to Prometheus one. I guess it is to complicated and unnecessary to copy scrapeClasses from Prometheus CRD: sometimes you will not be able to solve the problem with certs mounted to Prometheus/prometheus image itself. So i guess configuration of scrapeclasses of VMAgent should be on the user side. IF VMAgent dont have scrapeclass of Any CRD it collected, it will also ignore it ,like prometheus do with an event:
```
ServiceMonitor example-service-monitor was rejected due to invalid configuration:  
scrapeClass "istio" not found in Prometheus scrapeClasses
``` @f41gh7: @endesapt thanks for the initial implementation.  I created a small follow-up commit, which changes a bit field names at scrapeClass, most notable change are `relabelings` and `metricRelabelings` rename. It has a different names compared to the Prometheus, but I think, it's better to align it with our current naming at Scrape Objects.

 Also, I added OAuth2 and BasicAuth to the ScrapeClass. Feel free to provide any feedback on it. 

https://github.com/VictoriaMetrics/operator/commit/96d9c3447a558cc66b9bc7c67a967ccdc2d03422#diff-da13336d6df8ab1b3324185381d6d1ae3cb55ab3fafa2da17738cfd138ff088a@f41gh7:  It looks like scrapeClass merge logic should be a bit adjusted. Secret or ConfigMap auth options must have priority for `*File` related auth configurations.

 As an example:

scrapeClass: tlsConfig.Cert.Secret must have priority over podScrape.spec.endpoint.tlsConfig.CertFile. 

 It improves security for VMAgent. See linked issue.@endesapt: > scrapeClass: tlsConfig.Cert.Secret must have priority over podScrape.spec.endpoint.tlsConfig.CertFile.

1. Won't there be a situation if we have VMAgent with default scrapeClass and tlsConfig.Cert.Secret, and for example, we want to override it with CertFile (on some VMProbe resource), but we cant do that because of the priority? 

2. I think it is a little bit confusing that specs of resources sometimes will be overrided even if they exist, especially because if i understand it right, Prometheus Operator has the same behaviour we have now, and changing it may confuse users that migrate from it. 

So in general i think it is great, that TLS is not overrided by scrapeClass if it exists on resource side, even if it is not as secure, it gives flexibility to end user.

If i missed something, correct me @f41gh7: The feature was included into [v0.64.0](https://github.com/VictoriaMetrics/operator/releases/tag/v0.64.0) release

Solution patch:
diff --git a/api/operator/v1/vlcluster_types.go b/api/operator/v1/vlcluster_types.go
index 41a912d6..0ee297ff 100644
--- a/api/operator/v1/vlcluster_types.go
+++ b/api/operator/v1/vlcluster_types.go
@@ -277,6 +277,12 @@ func (sf *FieldsListString) UnmarshalJSON(src []byte) error {
 	return nil
 }
 
+// VLStorageNode defines slice of additional vlstorage nodes
+type VLStorageNode struct {
+	// Addr defines storage node address
+	Addr string `json:"addr"`
+}
+
 // VLSelect defines vlselect component configuration at victoria-logs cluster
 type VLSelect struct {
 	// PodMetadata configures Labels and Annotations which are propagated to the VLSelect pods.
@@ -313,6 +319,9 @@ type VLSelect struct {
 	// +optional
 	RollingUpdate *appsv1.RollingUpdateDeployment `json:"rollingUpdate,omitempty"`
 
+	// ExtraStorageNodes - defines additional storage nodes to VLSelect
+	ExtraStorageNodes []VLStorageNode `json:"extraStorageNodes,omitempty"`
+
 	vmv1beta1.CommonDefaultableParams           `json:",inline"`
 	vmv1beta1.CommonApplicationDeploymentParams `json:",inline"`
 }
diff --git a/api/operator/v1/vtcluster_types.go b/api/operator/v1/vtcluster_types.go
index f7bc3606..7c49a58b 100644
--- a/api/operator/v1/vtcluster_types.go
+++ b/api/operator/v1/vtcluster_types.go
@@ -189,6 +189,12 @@ func (cr *VTInsert) GetAdditionalService() *vmv1beta1.AdditionalServiceSpec {
 	return cr.ServiceSpec
 }
 
+// VTStorageNode defines slice of additional vtstorage nodes
+type VTStorageNode struct {
+	// Addr defines storage node address
+	Addr string `json:"addr"`
+}
+
 // VTSelect defines vtselect component configuration at victoria-traces cluster
 type VTSelect struct {
 	// PodMetadata configures Labels and Annotations which are propagated to the VTSelect pods.
@@ -225,6 +231,9 @@ type VTSelect struct {
 	// +optional
 	RollingUpdate *appsv1.RollingUpdateDeployment `json:"rollingUpdate,omitempty"`
 
+	// ExtraStorageNodes - defines additional storage nodes to VTSelect
+	ExtraStorageNodes []VTStorageNode `json:"extraStorageNodes,omitempty"`
+
 	vmv1beta1.CommonDefaultableParams           `json:",inline"`
 	vmv1beta1.CommonApplicationDeploymentParams `json:",inline"`
 }
diff --git a/api/operator/v1/zz_generated.deepcopy.go b/api/operator/v1/zz_generated.deepcopy.go
index f77ff8f3..de3c2694 100644
--- a/api/operator/v1/zz_generated.deepcopy.go
+++ b/api/operator/v1/zz_generated.deepcopy.go
@@ -678,6 +678,11 @@ func (in *VLSelect) DeepCopyInto(out *VLSelect) {
 		*out = new(appsv1.RollingUpdateDeployment)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.ExtraStorageNodes != nil {
+		in, out := &in.ExtraStorageNodes, &out.ExtraStorageNodes
+		*out = make([]VLStorageNode, len(*in))
+		copy(*out, *in)
+	}
 	in.CommonDefaultableParams.DeepCopyInto(&out.CommonDefaultableParams)
 	in.CommonApplicationDeploymentParams.DeepCopyInto(&out.CommonApplicationDeploymentParams)
 }
@@ -899,6 +904,21 @@ func (in *VLStorage) DeepCopy() *VLStorage {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VLStorageNode) DeepCopyInto(out *VLStorageNode) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VLStorageNode.
+func (in *VLStorageNode) DeepCopy() *VLStorageNode {
+	if in == nil {
+		return nil
+	}
+	out := new(VLStorageNode)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *VMAnomaly) DeepCopyInto(out *VMAnomaly) {
 	*out = *in
@@ -1452,6 +1472,11 @@ func (in *VTSelect) DeepCopyInto(out *VTSelect) {
 		*out = new(appsv1.RollingUpdateDeployment)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.ExtraStorageNodes != nil {
+		in, out := &in.ExtraStorageNodes, &out.ExtraStorageNodes
+		*out = make([]VTStorageNode, len(*in))
+		copy(*out, *in)
+	}
 	in.CommonDefaultableParams.DeepCopyInto(&out.CommonDefaultableParams)
 	in.CommonApplicationDeploymentParams.DeepCopyInto(&out.CommonApplicationDeploymentParams)
 }
@@ -1667,3 +1692,18 @@ func (in *VTStorage) DeepCopy() *VTStorage {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *VTStorageNode) DeepCopyInto(out *VTStorageNode) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VTStorageNode.
+func (in *VTStorageNode) DeepCopy() *VTStorageNode {
+	if in == nil {
+		return nil
+	}
+	out := new(VTStorageNode)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/api/operator/v1beta1/vmagent_types.go b/api/operator/v1beta1/vmagent_types.go
index 352bd1a1..27c7fbaf 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/vmcluster_types.go b/api/operator/v1beta1/vmcluster_types.go
index fd358bee..265ac6ca 100644
--- a/api/operator/v1beta1/vmcluster_types.go
+++ b/api/operator/v1beta1/vmcluster_types.go
@@ -239,12 +239,12 @@ type VMSelect struct {
 	// +optional
 	CacheMountPath string `json:"cacheMountPath,omitempty"`
 
-	// Storage - add persistent volume for cacheMountPath
+	// PersistentVolume - add persistent volume for cacheMountPath
 	// its useful for persistent cache
 	// use storage instead of persistentVolume.
 	// +deprecated
 	// +optional
-	Storage *StorageSpec `json:"persistentVolume,omitempty"`
+	PersistentVolume *StorageSpec `json:"persistentVolume,omitempty"`
 	// PersistentVolumeClaimRetentionPolicy allows configuration of PVC retention policy
 	// +optional
 	PersistentVolumeClaimRetentionPolicy *appsv1.StatefulSetPersistentVolumeClaimRetentionPolicy `json:"persistentVolumeClaimRetentionPolicy,omitempty"`
@@ -589,7 +589,7 @@ func (cr *VMStorage) GetStorageVolumeName() string {
 func (cr *VMSelect) GetCacheMountVolumeName() string {
 	storageSpec := cr.StorageSpec
 	if storageSpec == nil {
-		storageSpec = cr.Storage
+		storageSpec = cr.PersistentVolume
 	}
 	if storageSpec != nil && storageSpec.VolumeClaimTemplate.Name != "" {
 		return storageSpec.VolumeClaimTemplate.Name
diff --git a/api/operator/v1beta1/vmservicescrape_types.go b/api/operator/v1beta1/vmservicescrape_types.go
index 55be1a64..889e7b4c 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 f394bbe6..b3b56668 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)
@@ -6141,8 +6205,8 @@ func (in *VMSelect) DeepCopyInto(out *VMSelect) {
 		*out = new(EmbeddedObjectMetadata)
 		(*in).DeepCopyInto(*out)
 	}
-	if in.Storage != nil {
-		in, out := &in.Storage, &out.Storage
+	if in.PersistentVolume != nil {
+		in, out := &in.PersistentVolume, &out.PersistentVolume
 		*out = new(StorageSpec)
 		(*in).DeepCopyInto(*out)
 	}
@@ -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 94d0fb4b..ed8e606f 100644
--- a/config/crd/overlay/crd.yaml
+++ b/config/crd/overlay/crd.yaml
@@ -3481,6 +3481,20 @@ spec:
                           x-kubernetes-map-type: atomic
                       type: object
                     type: array
+                  extraStorageNodes:
+                    description: ExtraStorageNodes - defines additional storage nodes
+                      to VLSelect
+                    items:
+                      description: VLStorageNode defines slice of additional vlstorage
+                        nodes
+                      properties:
+                        addr:
+                          description: Addr defines storage node address
+                          type: string
+                      required:
+                      - addr
+                      type: object
+                    type: array
                   host_aliases:
                     description: |-
                       HostAliasesUnderScore provides mapping for ip and hostname,
@@ -9710,6 +9724,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.
@@ -26218,7 +26616,7 @@ spec:
                     type: boolean
                   persistentVolume:
                     description: |-
-                      Storage - add persistent volume for cacheMountPath
+                      PersistentVolume - add persistent volume for cacheMountPath
                       its useful for persistent cache
                       use storage instead of persistentVolume.
                     properties:
@@ -36022,6 +36420,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.
@@ -40710,6 +41111,20 @@ spec:
                           x-kubernetes-map-type: atomic
                       type: object
                     type: array
+                  extraStorageNodes:
+                    description: ExtraStorageNodes - defines additional storage nodes
+                      to VTSelect
+                    items:
+                      description: VTStorageNode defines slice of additional vtstorage
+                        nodes
+                      properties:
+                        addr:
+                          description: Addr defines storage node address
+                          type: string
+                      required:
+                      - addr
+                      type: object
+                    type: array
                   host_aliases:
                     description: |-
                       HostAliasesUnderScore provides mapping for ip and hostname,
diff --git a/config/rbac/kustomization.yaml b/config/rbac/kustomization.yaml
index 47ae84d9..166fe798 100644
--- a/config/rbac/kustomization.yaml
+++ b/config/rbac/kustomization.yaml
@@ -9,46 +9,3 @@ resources:
 - role_binding.yaml
 - leader_election_role.yaml
 - leader_election_role_binding.yaml
-# For each CRD, "Editor" and "Viewer" roles are scaffolded by
-# default, aiding admins in cluster management. Those roles are
-# not used by the Project itself. You can comment the following lines
-# if you do not want those helpers be installed with your Project.
-# - operator_vlagent_admin_role.yaml
-# - operator_vlagent_editor_role.yaml
-# - operator_vlagent_viewer_role.yaml
-# - operator_vlcluster_editor_role.yaml
-# - operator_vlcluster_viewer_role.yaml
-# - operator_vlsingle_editor_role.yaml
-# - operator_vlsingle_viewer_role.yaml
-# - operator_vlogs_editor_role.yaml
-# - operator_vlogs_viewer_role.yaml
-# - operator_vmscrapeconfig_editor_role.yaml
-# - operator_vmscrapeconfig_viewer_role.yaml
-# - operator_vmauth_editor_role.yaml
-# - operator_vmauth_viewer_role.yaml
-# - operator_vmuser_editor_role.yaml
-# - operator_vmuser_viewer_role.yaml
-# - operator_vmstaticscrape_editor_role.yaml
-# - operator_vmstaticscrape_viewer_role.yaml
-# - operator_vmnodescrape_editor_role.yaml
-# - operator_vmnodescrape_viewer_role.yaml
-# - operator_vmprobe_editor_role.yaml
-# - operator_vmprobe_viewer_role.yaml
-# - operator_vmcluster_editor_role.yaml
-# - operator_vmcluster_viewer_role.yaml
-# - operator_vmsingle_editor_role.yaml
-# - operator_vmsingle_viewer_role.yaml
-# - operator_vmservicescrape_editor_role.yaml
-# - operator_vmservicescrape_viewer_role.yaml
-# - operator_vmrule_editor_role.yaml
-# - operator_vmrule_viewer_role.yaml
-# - operator_vmpodscrape_editor_role.yaml
-# - operator_vmpodscrape_viewer_role.yaml
-# - operator_vmalertmanagerconfig_editor_role.yaml
-# - operator_vmalertmanagerconfig_viewer_role.yaml
-# - operator_vmalertmanager_editor_role.yaml
-# - operator_vmalertmanager_viewer_role.yaml
-# - operator_vmalert_editor_role.yaml
-# - operator_vmalert_viewer_role.yaml
-# - operator_vmagent_editor_role.yaml
-# - operator_vmagent_viewer_role.yaml
diff --git a/config/rbac/operator_vlagent_admin_role.yaml b/config/rbac/operator_vlagent_admin_role.yaml
deleted file mode 100644
index 78ce0bb3..00000000
--- a/config/rbac/operator_vlagent_admin_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# This rule is not used by the project victoriametrics-operator itself.
-# It is provided to allow the cluster admin to help manage permissions for users.
-#
-# Grants full permissions ('*') over operator.victoriametrics.com.
-# This role is intended for users authorized to modify roles and bindings within the cluster,
-# enabling them to delegate specific permissions to other users or groups as needed.
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlagent-admin-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlagents
-  verbs:
-  - '*'
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlagents/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlagent_editor_role.yaml b/config/rbac/operator_vlagent_editor_role.yaml
deleted file mode 100644
index 623318b3..00000000
--- a/config/rbac/operator_vlagent_editor_role.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-# This rule is not used by the project victoriametrics-operator itself.
-# It is provided to allow the cluster admin to help manage permissions for users.
-#
-# Grants permissions to create, update, and delete resources within the operator.victoriametrics.com.
-# This role is intended for users who need to manage these resources
-# but should not control RBAC or manage permissions for others.
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlagent-editor-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlagents
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlagents/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlagent_viewer_role.yaml b/config/rbac/operator_vlagent_viewer_role.yaml
deleted file mode 100644
index 69d68bc2..00000000
--- a/config/rbac/operator_vlagent_viewer_role.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-# This rule is not used by the project victoriametrics-operator itself.
-# It is provided to allow the cluster admin to help manage permissions for users.
-#
-# Grants read-only access to operator.victoriametrics.com resources.
-# This role is intended for users who need visibility into these resources
-# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlagent-viewer-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlagents
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlagents/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlcluster_editor_role.yaml b/config/rbac/operator_vlcluster_editor_role.yaml
deleted file mode 100644
index 19d55482..00000000
--- a/config/rbac/operator_vlcluster_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vlclusters.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlcluster-editor-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlclusters
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlclusters/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlcluster_viewer_role.yaml b/config/rbac/operator_vlcluster_viewer_role.yaml
deleted file mode 100644
index a984a574..00000000
--- a/config/rbac/operator_vlcluster_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vlclusters.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlcluster-viewer-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlclusters
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlclusters/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlogs_editor_role.yaml b/config/rbac/operator_vlogs_editor_role.yaml
deleted file mode 100644
index d554a00c..00000000
--- a/config/rbac/operator_vlogs_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vlogs.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlogs-editor-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlogs
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlogs/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlogs_viewer_role.yaml b/config/rbac/operator_vlogs_viewer_role.yaml
deleted file mode 100644
index fb2190d7..00000000
--- a/config/rbac/operator_vlogs_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vlogs.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlogs-viewer-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlogs
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlogs/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlsingle_editor_role.yaml b/config/rbac/operator_vlsingle_editor_role.yaml
deleted file mode 100644
index 62aaa82f..00000000
--- a/config/rbac/operator_vlsingle_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vlsingles.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlsingle-editor-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlsingles
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlsingles/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vlsingle_viewer_role.yaml b/config/rbac/operator_vlsingle_viewer_role.yaml
deleted file mode 100644
index 6a9e1156..00000000
--- a/config/rbac/operator_vlsingle_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vlsingles.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vlsingle-viewer-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlsingles
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vlsingles/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmagent_editor_role.yaml b/config/rbac/operator_vmagent_editor_role.yaml
deleted file mode 100644
index 63dc080c..00000000
--- a/config/rbac/operator_vmagent_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmagents.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmagent-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmagents
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmagents/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmagent_viewer_role.yaml b/config/rbac/operator_vmagent_viewer_role.yaml
deleted file mode 100644
index 407e81a9..00000000
--- a/config/rbac/operator_vmagent_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmagents.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmagent-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmagents
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmagents/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmalert_editor_role.yaml b/config/rbac/operator_vmalert_editor_role.yaml
deleted file mode 100644
index 7461421f..00000000
--- a/config/rbac/operator_vmalert_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmalerts.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmalert-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalerts
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalerts/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmalert_viewer_role.yaml b/config/rbac/operator_vmalert_viewer_role.yaml
deleted file mode 100644
index 6304f984..00000000
--- a/config/rbac/operator_vmalert_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmalerts.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmalert-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalerts
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalerts/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmalertmanager_editor_role.yaml b/config/rbac/operator_vmalertmanager_editor_role.yaml
deleted file mode 100644
index 5c91ba1b..00000000
--- a/config/rbac/operator_vmalertmanager_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmalertmanagers.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmalertmanager-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagers
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagers/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmalertmanager_viewer_role.yaml b/config/rbac/operator_vmalertmanager_viewer_role.yaml
deleted file mode 100644
index 3ec38c06..00000000
--- a/config/rbac/operator_vmalertmanager_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmalertmanagers.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmalertmanager-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagers
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagers/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmalertmanagerconfig_editor_role.yaml b/config/rbac/operator_vmalertmanagerconfig_editor_role.yaml
deleted file mode 100644
index 3b26133f..00000000
--- a/config/rbac/operator_vmalertmanagerconfig_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vmalertmanagerconfigs.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmalertmanagerconfig-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagerconfigs
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagerconfigs/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmalertmanagerconfig_viewer_role.yaml b/config/rbac/operator_vmalertmanagerconfig_viewer_role.yaml
deleted file mode 100644
index cc7dadb4..00000000
--- a/config/rbac/operator_vmalertmanagerconfig_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmalertmanagerconfigs.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmalertmanagerconfig-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagerconfigs
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmalertmanagerconfigs/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmanomaly_admin_role.yaml b/config/rbac/operator_vmanomaly_admin_role.yaml
deleted file mode 100644
index 3a3e2f95..00000000
--- a/config/rbac/operator_vmanomaly_admin_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# This rule is not used by the project victoriametrics-operator itself.
-# It is provided to allow the cluster admin to help manage permissions for users.
-#
-# Grants full permissions ('*') over operator.victoriametrics.com.
-# This role is intended for users authorized to modify roles and bindings within the cluster,
-# enabling them to delegate specific permissions to other users or groups as needed.
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmanomaly-admin-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmanomalies
-  verbs:
-  - '*'
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmanomalies/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmanomaly_editor_role.yaml b/config/rbac/operator_vmanomaly_editor_role.yaml
deleted file mode 100644
index 3ea84e9a..00000000
--- a/config/rbac/operator_vmanomaly_editor_role.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-# This rule is not used by the project victoriametrics-operator itself.
-# It is provided to allow the cluster admin to help manage permissions for users.
-#
-# Grants permissions to create, update, and delete resources within the operator.victoriametrics.com.
-# This role is intended for users who need to manage these resources
-# but should not control RBAC or manage permissions for others.
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmanomaly-editor-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmanomalies
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmanomalies/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmanomaly_viewer_role.yaml b/config/rbac/operator_vmanomaly_viewer_role.yaml
deleted file mode 100644
index f1f2fa1e..00000000
--- a/config/rbac/operator_vmanomaly_viewer_role.yaml
+++ /dev/null
@@ -1,29 +0,0 @@
-# This rule is not used by the project victoriametrics-operator itself.
-# It is provided to allow the cluster admin to help manage permissions for users.
-#
-# Grants read-only access to operator.victoriametrics.com resources.
-# This role is intended for users who need visibility into these resources
-# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.
-
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: victoriametrics-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmanomaly-viewer-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmanomalies
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmanomalies/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmauth_editor_role.yaml b/config/rbac/operator_vmauth_editor_role.yaml
deleted file mode 100644
index f06d750a..00000000
--- a/config/rbac/operator_vmauth_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vmauths.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmauth-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmauths
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmauths/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmauth_viewer_role.yaml b/config/rbac/operator_vmauth_viewer_role.yaml
deleted file mode 100644
index 24f30467..00000000
--- a/config/rbac/operator_vmauth_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmauths.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmauth-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmauths
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmauths/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmcluster_editor_role.yaml b/config/rbac/operator_vmcluster_editor_role.yaml
deleted file mode 100644
index 2fa71262..00000000
--- a/config/rbac/operator_vmcluster_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vmclusters.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmcluster-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmclusters
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmclusters/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmcluster_viewer_role.yaml b/config/rbac/operator_vmcluster_viewer_role.yaml
deleted file mode 100644
index d0b98fc5..00000000
--- a/config/rbac/operator_vmcluster_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmclusters.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmcluster-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmclusters
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmclusters/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmnodescrape_editor_role.yaml b/config/rbac/operator_vmnodescrape_editor_role.yaml
deleted file mode 100644
index 13219b24..00000000
--- a/config/rbac/operator_vmnodescrape_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmnodescrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmnodescrape-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmnodescrapes
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmnodescrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmnodescrape_viewer_role.yaml b/config/rbac/operator_vmnodescrape_viewer_role.yaml
deleted file mode 100644
index 1777b97f..00000000
--- a/config/rbac/operator_vmnodescrape_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmnodescrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmnodescrape-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmnodescrapes
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmnodescrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmpodscrape_editor_role.yaml b/config/rbac/operator_vmpodscrape_editor_role.yaml
deleted file mode 100644
index ee258c5a..00000000
--- a/config/rbac/operator_vmpodscrape_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmpodscrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmpodscrape-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmpodscrapes
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmpodscrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmpodscrape_viewer_role.yaml b/config/rbac/operator_vmpodscrape_viewer_role.yaml
deleted file mode 100644
index ed5bef33..00000000
--- a/config/rbac/operator_vmpodscrape_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmpodscrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmpodscrape-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmpodscrapes
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmpodscrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmprobe_editor_role.yaml b/config/rbac/operator_vmprobe_editor_role.yaml
deleted file mode 100644
index ee45ea3e..00000000
--- a/config/rbac/operator_vmprobe_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmprobes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmprobe-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmprobes
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmprobes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmprobe_viewer_role.yaml b/config/rbac/operator_vmprobe_viewer_role.yaml
deleted file mode 100644
index d08e4ccb..00000000
--- a/config/rbac/operator_vmprobe_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmprobes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmprobe-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmprobes
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmprobes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmrule_editor_role.yaml b/config/rbac/operator_vmrule_editor_role.yaml
deleted file mode 100644
index 77bd687f..00000000
--- a/config/rbac/operator_vmrule_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmrules.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmrule-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmrules
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmrules/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmrule_viewer_role.yaml b/config/rbac/operator_vmrule_viewer_role.yaml
deleted file mode 100644
index 070665ad..00000000
--- a/config/rbac/operator_vmrule_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmrules.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmrule-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmrules
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmrules/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmscrapeconfig_editor_role.yaml b/config/rbac/operator_vmscrapeconfig_editor_role.yaml
deleted file mode 100644
index 11299cb4..00000000
--- a/config/rbac/operator_vmscrapeconfig_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vmscrapeconfigs.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmscrapeconfig-editor-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmscrapeconfigs
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmscrapeconfigs/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmscrapeconfig_viewer_role.yaml b/config/rbac/operator_vmscrapeconfig_viewer_role.yaml
deleted file mode 100644
index 1e3ce93b..00000000
--- a/config/rbac/operator_vmscrapeconfig_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmscrapeconfigs.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmscrapeconfig-viewer-role
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmscrapeconfigs
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmscrapeconfigs/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmservicescrape_editor_role.yaml b/config/rbac/operator_vmservicescrape_editor_role.yaml
deleted file mode 100644
index 1740e0b7..00000000
--- a/config/rbac/operator_vmservicescrape_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmservicescrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmservicescrape-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmservicescrapes
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmservicescrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmservicescrape_viewer_role.yaml b/config/rbac/operator_vmservicescrape_viewer_role.yaml
deleted file mode 100644
index fc2058fa..00000000
--- a/config/rbac/operator_vmservicescrape_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmservicescrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmservicescrape-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmservicescrapes
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmservicescrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmsingle_editor_role.yaml b/config/rbac/operator_vmsingle_editor_role.yaml
deleted file mode 100644
index 9c606099..00000000
--- a/config/rbac/operator_vmsingle_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmsingles.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmsingle-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmsingles
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmsingles/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmsingle_viewer_role.yaml b/config/rbac/operator_vmsingle_viewer_role.yaml
deleted file mode 100644
index 07491a72..00000000
--- a/config/rbac/operator_vmsingle_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmsingles.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmsingle-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmsingles
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmsingles/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmstaticscrape_editor_role.yaml b/config/rbac/operator_vmstaticscrape_editor_role.yaml
deleted file mode 100644
index 78062fd9..00000000
--- a/config/rbac/operator_vmstaticscrape_editor_role.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-# permissions for end users to edit vmstaticscrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmstaticscrape-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmstaticscrapes
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-  - deletecollection
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmstaticscrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmstaticscrape_viewer_role.yaml b/config/rbac/operator_vmstaticscrape_viewer_role.yaml
deleted file mode 100644
index 98575aa5..00000000
--- a/config/rbac/operator_vmstaticscrape_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmstaticscrapes.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmstaticscrape-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmstaticscrapes
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmstaticscrapes/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmuser_editor_role.yaml b/config/rbac/operator_vmuser_editor_role.yaml
deleted file mode 100644
index a2c8233c..00000000
--- a/config/rbac/operator_vmuser_editor_role.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-# permissions for end users to edit vmusers.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmuser-editor
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmusers
-  verbs:
-  - create
-  - delete
-  - get
-  - list
-  - patch
-  - update
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmusers/status
-  verbs:
-  - get
diff --git a/config/rbac/operator_vmuser_viewer_role.yaml b/config/rbac/operator_vmuser_viewer_role.yaml
deleted file mode 100644
index 55950b84..00000000
--- a/config/rbac/operator_vmuser_viewer_role.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-# permissions for end users to view vmusers.
-apiVersion: rbac.authorization.k8s.io/v1
-kind: ClusterRole
-metadata:
-  labels:
-    app.kubernetes.io/name: vm-operator
-    app.kubernetes.io/managed-by: kustomize
-  name: operator-vmuser-viewer
-rules:
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmusers
-  verbs:
-  - get
-  - list
-  - watch
-- apiGroups:
-  - operator.victoriametrics.com
-  resources:
-  - vmusers/status
-  verbs:
-  - get
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index e891c4d3..043e9c2e 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -22,8 +22,13 @@ aliases:
 * FEATURE: [vmoperator](https://docs.victoriametrics.com/operator/): add `unhealthyPodEvictionPolicy` to the `podDisruptionBudget` specification. See this issue [#1534](https://github.com/VictoriaMetrics/operator/issues/1534) for details.
 * FEATURE: [vmoperator](https://docs.victoriametrics.com/operator/): add `rollingUpdate` and `updateStrategy` fields to `VMAuth.spec`, `VMCluster.spec.requestsLoadBalancer.spec`, `VLCluster.spec.requestsLoadBalancer.spec` and `VTCluster.spec.requestsLoadBalancer.spec`. See this issue [#1540](https://github.com/VictoriaMetrics/operator/issues/1540) for details.
 * FEATURE: [vmoperator](https://docs.victoriametrics.com/operator/): preserve 3rd party `labels` on object during `reconcile`. Previously, operator allowed to keep only `managedMetadata.labels`. See this issue [#1533](https://github.com/VictoriaMetrics/operator/issues/1533). Thanks to the @lllamnyp
+* FEATURE: [VLCluster](https://docs.victoriametrics.com/operator/resources/vlcluster/): added `spec.vlselect.extraStorageNodes` to specify list of additional storage nodes, that are available for select only.
+* FEATURE: [VTCluster](https://docs.victoriametrics.com/operator/resources/vtcluster/): added `spec.vtselect.extraStorageNodes` to specify list of additional storage nodes, that are available for select only.
 
 * BUGFIX: [vmoperator](https://docs.victoriametrics.com/operator/): fix an issue where the return value from a couple of controllers was always `nil`. See [#1532](https://github.com/VictoriaMetrics/operator/pull/1532) for details.
+* BUGFIX: [VMCluster](https://docs.victoriametrics.com/operator/resources/vmcluster/): emit warning if `vmcluster.spec.vmselect.persistentVolume` is set, previously it was emitted for `vmcluster.spec.vmselect.storage`.
+* BUGFIX: [vmoperator](https://docs.victoriametrics.com/operator/): Prevent endless Service reconcile loop by correctly track changes to Service.spec.LoadBalancerClass. See this issue [#1550](https://github.com/VictoriaMetrics/operator/issues/1550) for details.
+
 
 ## [v0.63.0](https://github.com/VictoriaMetrics/operator/releases/tag/v0.63.0)
 
diff --git a/docs/api.md b/docs/api.md
index 55a42149..bffd4e35 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -393,6 +393,7 @@ Appears in: [VLClusterSpec](#vlclusterspec)
 | extraArgs<a href="#vlselect-extraargs" id="vlselect-extraargs">#</a><br/>_object (keys:string, values:string)_ | _(Optional)_<br/>ExtraArgs that will be passed to the application container<br />for example remoteWrite.tmpDataPath: /tmp |
 | extraEnvs<a href="#vlselect-extraenvs" id="vlselect-extraenvs">#</a><br/>_[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envvar-v1-core) array_ | _(Optional)_<br/>ExtraEnvs that will be passed to the application container |
 | extraEnvsFrom<a href="#vlselect-extraenvsfrom" id="vlselect-extraenvsfrom">#</a><br/>_[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envfromsource-v1-core) array_ | _(Optional)_<br/>ExtraEnvsFrom defines source of env variables for the application container<br />could either be secret or configmap |
+| extraStorageNodes<a href="#vlselect-extrastoragenodes" id="vlselect-extrastoragenodes">#</a><br/>_[VLStorageNode](#vlstoragenode) array_ | _(Required)_<br/>ExtraStorageNodes - defines additional storage nodes to VLSelect |
 | hostAliases<a href="#vlselect-hostaliases" id="vlselect-hostaliases">#</a><br/>_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#hostalias-v1-core) array_ | _(Optional)_<br/>HostAliases provides mapping for ip and hostname,<br />that would be propagated to pod,<br />cannot be used with HostNetwork. |
 | hostNetwork<a href="#vlselect-hostnetwork" id="vlselect-hostnetwork">#</a><br/>_boolean_ | _(Optional)_<br/>HostNetwork controls whether the pod may use the node network namespace |
 | host_aliases<a href="#vlselect-host_aliases" id="vlselect-host_aliases">#</a><br/>_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#hostalias-v1-core) array_ | _(Optional)_<br/>HostAliasesUnderScore provides mapping for ip and hostname,<br />that would be propagated to pod,<br />cannot be used with HostNetwork.<br />Has Priority over hostAliases field |
@@ -580,6 +581,19 @@ Appears in: [VLClusterSpec](#vlclusterspec)
 | volumes<a href="#vlstorage-volumes" id="vlstorage-volumes">#</a><br/>_[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | _(Required)_<br/>Volumes allows configuration of additional volumes on the output Deployment/StatefulSet definition.<br />Volumes specified will be appended to other volumes that are generated.<br />/ +optional |
 
 
+#### VLStorageNode
+
+
+
+VLStorageNode defines slice of additional vlstorage nodes
+
+Appears in: [VLSelect](#vlselect)
+
+| Field | Description |
+| --- | --- |
+| addr<a href="#vlstoragenode-addr" id="vlstoragenode-addr">#</a><br/>_string_ | _(Required)_<br/>Addr defines storage node address |
+
+
 #### VMAnomaly
 
 
@@ -634,6 +648,7 @@ Appears in: [VMAnomalyMonitoringSpec](#vmanomalymonitoringspec)
 
 VMAnomalyMonitoringPushSpec defines metrics push configuration
 
+
 VMAnomaly uses prometheus text exposition format
 
 Appears in: [VMAnomalyMonitoringSpec](#vmanomalymonitoringspec)
@@ -913,6 +928,7 @@ Appears in: [VTClusterSpec](#vtclusterspec)
 | extraArgs<a href="#vtselect-extraargs" id="vtselect-extraargs">#</a><br/>_object (keys:string, values:string)_ | _(Optional)_<br/>ExtraArgs that will be passed to the application container<br />for example remoteWrite.tmpDataPath: /tmp |
 | extraEnvs<a href="#vtselect-extraenvs" id="vtselect-extraenvs">#</a><br/>_[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envvar-v1-core) array_ | _(Optional)_<br/>ExtraEnvs that will be passed to the application container |
 | extraEnvsFrom<a href="#vtselect-extraenvsfrom" id="vtselect-extraenvsfrom">#</a><br/>_[EnvFromSource](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envfromsource-v1-core) array_ | _(Optional)_<br/>ExtraEnvsFrom defines source of env variables for the application container<br />could either be secret or configmap |
+| extraStorageNodes<a href="#vtselect-extrastoragenodes" id="vtselect-extrastoragenodes">#</a><br/>_[VTStorageNode](#vtstoragenode) array_ | _(Required)_<br/>ExtraStorageNodes - defines additional storage nodes to VTSelect |
 | hostAliases<a href="#vtselect-hostaliases" id="vtselect-hostaliases">#</a><br/>_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#hostalias-v1-core) array_ | _(Optional)_<br/>HostAliases provides mapping for ip and hostname,<br />that would be propagated to pod,<br />cannot be used with HostNetwork. |
 | hostNetwork<a href="#vtselect-hostnetwork" id="vtselect-hostnetwork">#</a><br/>_boolean_ | _(Optional)_<br/>HostNetwork controls whether the pod may use the node network namespace |
 | host_aliases<a href="#vtselect-host_aliases" id="vtselect-host_aliases">#</a><br/>_[HostAlias](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#hostalias-v1-core) array_ | _(Optional)_<br/>HostAliasesUnderScore provides mapping for ip and hostname,<br />that would be propagated to pod,<br />cannot be used with HostNetwork.<br />Has Priority over hostAliases field |
@@ -1099,6 +1115,19 @@ Appears in: [VTClusterSpec](#vtclusterspec)
 | volumes<a href="#vtstorage-volumes" id="vtstorage-volumes">#</a><br/>_[Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#volume-v1-core) array_ | _(Required)_<br/>Volumes allows configuration of additional volumes on the output Deployment/StatefulSet definition.<br />Volumes specified will be appended to other volumes that are generated.<br />/ +optional |
 
 
+#### VTStorageNode
+
+
+
+VTStorageNode defines slice of additional vtstorage nodes
+
+Appears in: [VTSelect](#vtselect)
+
+| Field | Description |
+| --- | --- |
+| addr<a href="#vtstoragenode-addr" id="vtstoragenode-addr">#</a><br/>_string_ | _(Required)_<br/>Addr defines storage node address |
+
+
 
 ## operator.victoriametrics.com/v1beta1
 
@@ -1472,7 +1501,6 @@ Appears in: [VMAlertmanagerSpec](#vmalertmanagerspec)
 | Field | Description |
 | --- | --- |
 | key<a href="#configmapkeyreference-key" id="configmapkeyreference-key">#</a><br/>_string_ | _(Required)_<br/>The ConfigMap key to refer to. |
-| name<a href="#configmapkeyreference-name" id="configmapkeyreference-name">#</a><br/>_string_ | _(Optional)_<br/>Name of the referent.<br />This field is effectively required, but due to backwards compatibility is<br />allowed to be empty. Instances of this type with an empty value here are<br />almost certainly wrong.<br />More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names |
 
 
 #### ConsulSDConfig
@@ -1740,7 +1768,7 @@ Appears in: [VLAgentSpec](#vlagentspec), [VLInsert](#vlinsert), [VLSelect](#vlse
 | maxUnavailable<a href="#embeddedpoddisruptionbudgetspec-maxunavailable" id="embeddedpoddisruptionbudgetspec-maxunavailable">#</a><br/>_[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#intorstring-intstr-util)_ | _(Optional)_<br/>An eviction is allowed if at most "maxUnavailable" pods selected by<br />"selector" are unavailable after the eviction, i.e. even in absence of<br />the evicted pod. For example, one can prevent all voluntary evictions<br />by specifying 0. This is a mutually exclusive setting with "minAvailable". |
 | minAvailable<a href="#embeddedpoddisruptionbudgetspec-minavailable" id="embeddedpoddisruptionbudgetspec-minavailable">#</a><br/>_[IntOrString](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#intorstring-intstr-util)_ | _(Optional)_<br/>An eviction is allowed if at least "minAvailable" pods selected by<br />"selector" will still be available after the eviction, i.e. even in the<br />absence of the evicted pod.  So for example you can prevent all voluntary<br />evictions by specifying "100%". |
 | selectorLabels<a href="#embeddedpoddisruptionbudgetspec-selectorlabels" id="embeddedpoddisruptionbudgetspec-selectorlabels">#</a><br/>_object (keys:string, values:string)_ | _(Optional)_<br/>replaces default labels selector generated by operator<br />it's useful when you need to create custom budget |
-| unhealthyPodEvictionPolicy<a href="#embeddedpoddisruptionbudgetspec-unhealthypodevictionpolicy" id="embeddedpoddisruptionbudgetspec-unhealthypodevictionpolicy">#</a><br/>_string_ | _(Optional)_<br/>UnhealthyPodEvictionPolicy defines the criteria for when unhealthy pods<br />Valid policies are IfHealthyBudget and AlwaysAllow.<br />If no policy is specified, the default behavior will be used,<br />which corresponds to the IfHealthyBudget policy.<br />Available from operator v0.64.0 |
+| unhealthyPodEvictionPolicy<a href="#embeddedpoddisruptionbudgetspec-unhealthypodevictionpolicy" id="embeddedpoddisruptionbudgetspec-unhealthypodevictionpolicy">#</a><br/>_string_ | _(Optional)_<br/>UnhealthyPodEvictionPolicy defines the criteria for when unhealthy pods<br /><br />Valid policies are IfHealthyBudget and AlwaysAllow.<br />If no policy is specified, the default behavior will be used,<br />which corresponds to the IfHealthyBudget policy.<br />Available from operator v0.64.0 |
 
 
 #### EmbeddedProbes
@@ -1892,6 +1920,7 @@ The private IP address is used by default, but may be changed to
 the public IP address with relabeling.
 See [here](https://docs.victoriametrics.com/victoriametrics/sd_configs/#gce_sd_configs)
 
+
 The GCE service discovery will load the Google Cloud credentials
 from the file specified by the GOOGLE_APPLICATION_CREDENTIALS environment variable.
 See https://cloud.google.com/kubernetes-engine/docs/tutorials/authenticating-to-cloud-platform
@@ -2895,9 +2924,9 @@ Appears in: [StreamAggrConfig](#streamaggrconfig)
 
 | Field | Description |
 | --- | --- |
-| by<a href="#streamaggrrule-by" id="streamaggrrule-by">#</a><br/>_string array_ | _(Optional)_<br/>By is an optional list of labels for grouping input series.<br />See also Without.<br />If neither By nor Without are set, then the Outputs are calculated<br />individually per each input time series. |
+| by<a href="#streamaggrrule-by" id="streamaggrrule-by">#</a><br/>_string array_ | _(Optional)_<br/>By is an optional list of labels for grouping input series.<br /><br />See also Without.<br /><br />If neither By nor Without are set, then the Outputs are calculated<br />individually per each input time series. |
 | dedup_interval<a href="#streamaggrrule-dedup_interval" id="streamaggrrule-dedup_interval">#</a><br/>_string_ | _(Optional)_<br/>DedupInterval is an optional interval for deduplication. |
-| drop_input_labels<a href="#streamaggrrule-drop_input_labels" id="streamaggrrule-drop_input_labels">#</a><br/>_string_ | _(Optional)_<br/>DropInputLabels is an optional list with labels, which must be dropped before further processing of input samples.<br />Labels are dropped before de-duplication and aggregation. |
+| drop_input_labels<a href="#streamaggrrule-drop_input_labels" id="streamaggrrule-drop_input_labels">#</a><br/>_string_ | _(Optional)_<br/>DropInputLabels is an optional list with labels, which must be dropped before further processing of input samples.<br /><br />Labels are dropped before de-duplication and aggregation. |
 | enable_windows<a href="#streamaggrrule-enable_windows" id="streamaggrrule-enable_windows">#</a><br/>_boolean_ | _(Optional)_<br/>EnableWindows enables aggregating data in separate windows |
 | flush_on_shutdown<a href="#streamaggrrule-flush_on_shutdown" id="streamaggrrule-flush_on_shutdown">#</a><br/>_boolean_ | _(Optional)_<br/>FlushOnShutdown defines whether to flush the aggregation state on process termination<br />or config reload. Is `false` by default.<br />It is not recommended changing this setting, unless unfinished aggregations states<br />are preferred to missing data points. |
 | ignoreFirstSampleInterval<a href="#streamaggrrule-ignorefirstsampleinterval" id="streamaggrrule-ignorefirstsampleinterval">#</a><br/>_string_ | _(Required)_<br/>IgnoreFirstSampleInterval sets interval for total and prometheus_total during which first samples will be ignored |
@@ -2906,12 +2935,12 @@ Appears in: [StreamAggrConfig](#streamaggrconfig)
 | input_relabel_configs<a href="#streamaggrrule-input_relabel_configs" id="streamaggrrule-input_relabel_configs">#</a><br/>_[RelabelConfig](#relabelconfig) array_ | _(Optional)_<br/>InputRelabelConfigs is an optional relabeling rules, which are applied on the input<br />before aggregation. |
 | interval<a href="#streamaggrrule-interval" id="streamaggrrule-interval">#</a><br/>_string_ | _(Required)_<br/>Interval is the interval between aggregations. |
 | keep_metric_names<a href="#streamaggrrule-keep_metric_names" id="streamaggrrule-keep_metric_names">#</a><br/>_boolean_ | _(Optional)_<br/>KeepMetricNames instructs to leave metric names as is for the output time series without adding any suffix. |
-| match<a href="#streamaggrrule-match" id="streamaggrrule-match">#</a><br/>_[StringOrArray](#stringorarray)_ | _(Optional)_<br/>Match is a label selector (or list of label selectors) for filtering time series for the given selector.<br />If the match isn't set, then all the input time series are processed. |
+| match<a href="#streamaggrrule-match" id="streamaggrrule-match">#</a><br/>_[StringOrArray](#stringorarray)_ | _(Optional)_<br/>Match is a label selector (or list of label selectors) for filtering time series for the given selector.<br /><br />If the match isn't set, then all the input time series are processed. |
 | no_align_flush_to_interval<a href="#streamaggrrule-no_align_flush_to_interval" id="streamaggrrule-no_align_flush_to_interval">#</a><br/>_boolean_ | _(Optional)_<br/>NoAlignFlushToInterval disables aligning of flushes to multiples of Interval.<br />By default flushes are aligned to Interval. |
 | output_relabel_configs<a href="#streamaggrrule-output_relabel_configs" id="streamaggrrule-output_relabel_configs">#</a><br/>_[RelabelConfig](#relabelconfig) array_ | _(Optional)_<br/>OutputRelabelConfigs is an optional relabeling rules, which are applied<br />on the aggregated output before being sent to remote storage. |
-| outputs<a href="#streamaggrrule-outputs" id="streamaggrrule-outputs">#</a><br/>_string array_ | _(Required)_<br/>Outputs is a list of output aggregate functions to produce.<br />The following names are allowed:<br />- total - aggregates input counters<br />- increase - counts the increase over input counters<br />- count_series - counts the input series<br />- count_samples - counts the input samples<br />- sum_samples - sums the input samples<br />- last - the last biggest sample value<br />- min - the minimum sample value<br />- max - the maximum sample value<br />- avg - the average value across all the samples<br />- stddev - standard deviation across all the samples<br />- stdvar - standard variance across all the samples<br />- histogram_bucket - creates VictoriaMetrics histogram for input samples<br />- quantiles(phi1, ..., phiN) - quantiles' estimation for phi in the range [0..1]<br />The output time series will have the following names:<br />  input_name:aggr_<interval>_<output> |
+| outputs<a href="#streamaggrrule-outputs" id="streamaggrrule-outputs">#</a><br/>_string array_ | _(Required)_<br/>Outputs is a list of output aggregate functions to produce.<br /><br />The following names are allowed:<br /><br />- total - aggregates input counters<br />- increase - counts the increase over input counters<br />- count_series - counts the input series<br />- count_samples - counts the input samples<br />- sum_samples - sums the input samples<br />- last - the last biggest sample value<br />- min - the minimum sample value<br />- max - the maximum sample value<br />- avg - the average value across all the samples<br />- stddev - standard deviation across all the samples<br />- stdvar - standard variance across all the samples<br />- histogram_bucket - creates VictoriaMetrics histogram for input samples<br />- quantiles(phi1, ..., phiN) - quantiles' estimation for phi in the range [0..1]<br /><br />The output time series will have the following names:<br /><br />  input_name:aggr_<interval>_<output> |
 | staleness_interval<a href="#streamaggrrule-staleness_interval" id="streamaggrrule-staleness_interval">#</a><br/>_string_ | _(Optional)_<br/>Staleness interval is interval after which the series state will be reset if no samples have been sent during it.<br />The parameter is only relevant for outputs: total, total_prometheus, increase, increase_prometheus and histogram_bucket. |
-| without<a href="#streamaggrrule-without" id="streamaggrrule-without">#</a><br/>_string array_ | _(Optional)_<br/>Without is an optional list of labels, which must be excluded when grouping input series.<br />See also By.<br />If neither By nor Without are set, then the Outputs are calculated<br />individually per each input time series. |
+| without<a href="#streamaggrrule-without" id="streamaggrrule-without">#</a><br/>_string array_ | _(Optional)_<br/>Without is an optional list of labels, which must be excluded when grouping input series.<br /><br />See also By.<br /><br />If neither By nor Without are set, then the Outputs are calculated<br />individually per each input time series. |
 
 
 #### StringOrArray
@@ -3882,7 +3911,7 @@ Appears in: [VMAuth](#vmauth)
 | dnsConfig<a href="#vmauthspec-dnsconfig" id="vmauthspec-dnsconfig">#</a><br/>_[PodDNSConfig](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#poddnsconfig-v1-core)_ | _(Optional)_<br/>Specifies the DNS parameters of a pod.<br />Parameters specified here will be merged to the generated DNS<br />configuration based on DNSPolicy. |
 | dnsPolicy<a href="#vmauthspec-dnspolicy" id="vmauthspec-dnspolicy">#</a><br/>_[DNSPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#dnspolicy-v1-core)_ | _(Optional)_<br/>DNSPolicy sets DNS policy for the pod |
 | drop_src_path_prefix_parts<a href="#vmauthspec-drop_src_path_prefix_parts" id="vmauthspec-drop_src_path_prefix_parts">#</a><br/>_integer_ | _(Optional)_<br/>DropSrcPathPrefixParts is the number of `/`-delimited request path prefix parts to drop before proxying the request to backend.<br />See [here](https://docs.victoriametrics.com/victoriametrics/vmauth/#dropping-request-path-prefix) for more details. |
-| dump_request_on_errors<a href="#vmauthspec-dump_request_on_errors" id="vmauthspec-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br />available since v1.107.0 vmauth version |
+| dump_request_on_errors<a href="#vmauthspec-dump_request_on_errors" id="vmauthspec-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br /><br />available since v1.107.0 vmauth version |
 | externalConfig<a href="#vmauthspec-externalconfig" id="vmauthspec-externalconfig">#</a><br/>_[ExternalConfig](#externalconfig)_ | _(Optional)_<br/>ExternalConfig defines a source of external VMAuth configuration.<br />If it's defined, configuration for vmauth becomes unmanaged and operator'll not create any related secrets/config-reloaders |
 | extraArgs<a href="#vmauthspec-extraargs" id="vmauthspec-extraargs">#</a><br/>_object (keys:string, values:string)_ | _(Optional)_<br/>ExtraArgs that will be passed to the application container<br />for example remoteWrite.tmpDataPath: /tmp |
 | extraEnvs<a href="#vmauthspec-extraenvs" id="vmauthspec-extraenvs">#</a><br/>_[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#envvar-v1-core) array_ | _(Optional)_<br/>ExtraEnvs that will be passed to the application container |
@@ -3929,7 +3958,7 @@ Appears in: [VMAuth](#vmauth)
 | tlsConfig<a href="#vmauthspec-tlsconfig" id="vmauthspec-tlsconfig">#</a><br/>_[TLSConfig](#tlsconfig)_ | _(Optional)_<br/>TLSConfig defines tls configuration for the backend connection |
 | tolerations<a href="#vmauthspec-tolerations" id="vmauthspec-tolerations">#</a><br/>_[Toleration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#toleration-v1-core) array_ | _(Optional)_<br/>Tolerations If specified, the pod's tolerations. |
 | topologySpreadConstraints<a href="#vmauthspec-topologyspreadconstraints" id="vmauthspec-topologyspreadconstraints">#</a><br/>_[TopologySpreadConstraint](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#topologyspreadconstraint-v1-core) array_ | _(Optional)_<br/>TopologySpreadConstraints embedded kubernetes pod configuration option,<br />controls how pods are spread across your cluster among failure-domains<br />such as regions, zones, nodes, and other user-defined topology domains<br />https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/ |
-| unauthorizedAccessConfig<a href="#vmauthspec-unauthorizedaccessconfig" id="vmauthspec-unauthorizedaccessconfig">#</a><br/>_[UnauthorizedAccessConfigURLMap](#unauthorizedaccessconfigurlmap) array_ | _(Required)_<br/>UnauthorizedAccessConfig configures access for un authorized users<br />Deprecated: use unauthorizedUserAccessSpec instead<br />will be removed at v1.0 release |
+| unauthorizedAccessConfig<a href="#vmauthspec-unauthorizedaccessconfig" id="vmauthspec-unauthorizedaccessconfig">#</a><br/>_[UnauthorizedAccessConfigURLMap](#unauthorizedaccessconfigurlmap) array_ | _(Required)_<br/>UnauthorizedAccessConfig configures access for un authorized users<br /><br />Deprecated: use unauthorizedUserAccessSpec instead<br />will be removed at v1.0 release |
 | unauthorizedUserAccessSpec<a href="#vmauthspec-unauthorizeduseraccessspec" id="vmauthspec-unauthorizeduseraccessspec">#</a><br/>_[VMAuthUnauthorizedUserAccessSpec](#vmauthunauthorizeduseraccessspec)_ | _(Optional)_<br/>UnauthorizedUserAccessSpec defines unauthorized_user config section of vmauth config |
 | updateStrategy<a href="#vmauthspec-updatestrategy" id="vmauthspec-updatestrategy">#</a><br/>_[DeploymentStrategyType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#deploymentstrategytype-v1-apps)_ | _(Optional)_<br/>UpdateStrategy - overrides default update strategy.<br />Available from operator v0.64.0 |
 | useDefaultResources<a href="#vmauthspec-usedefaultresources" id="vmauthspec-usedefaultresources">#</a><br/>_boolean_ | _(Optional)_<br/>UseDefaultResources controls resource settings<br />By default, operator sets built-in resource requirements |
@@ -3957,7 +3986,7 @@ Appears in: [VMAuthSpec](#vmauthspec)
 | default_url<a href="#vmauthunauthorizeduseraccessspec-default_url" id="vmauthunauthorizeduseraccessspec-default_url">#</a><br/>_string array_ | _(Required)_<br/>DefaultURLs backend url for non-matching paths filter<br />usually used for default backend with error message |
 | discover_backend_ips<a href="#vmauthunauthorizeduseraccessspec-discover_backend_ips" id="vmauthunauthorizeduseraccessspec-discover_backend_ips">#</a><br/>_boolean_ | _(Required)_<br/>DiscoverBackendIPs instructs discovering URLPrefix backend IPs via DNS. |
 | drop_src_path_prefix_parts<a href="#vmauthunauthorizeduseraccessspec-drop_src_path_prefix_parts" id="vmauthunauthorizeduseraccessspec-drop_src_path_prefix_parts">#</a><br/>_integer_ | _(Optional)_<br/>DropSrcPathPrefixParts is the number of `/`-delimited request path prefix parts to drop before proxying the request to backend.<br />See [here](https://docs.victoriametrics.com/victoriametrics/vmauth/#dropping-request-path-prefix) for more details. |
-| dump_request_on_errors<a href="#vmauthunauthorizeduseraccessspec-dump_request_on_errors" id="vmauthunauthorizeduseraccessspec-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br />available since v1.107.0 vmauth version |
+| dump_request_on_errors<a href="#vmauthunauthorizeduseraccessspec-dump_request_on_errors" id="vmauthunauthorizeduseraccessspec-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br /><br />available since v1.107.0 vmauth version |
 | headers<a href="#vmauthunauthorizeduseraccessspec-headers" id="vmauthunauthorizeduseraccessspec-headers">#</a><br/>_string array_ | _(Optional)_<br/>Headers represent additional http headers, that vmauth uses<br />in form of ["header_key: header_value"]<br />multiple values for header key:<br />["header_key: value1,value2"]<br />it's available since 1.68.0 version of vmauth |
 | ip_filters<a href="#vmauthunauthorizeduseraccessspec-ip_filters" id="vmauthunauthorizeduseraccessspec-ip_filters">#</a><br/>_[VMUserIPFilters](#vmuseripfilters)_ | _(Optional)_<br/>IPFilters defines per target src ip filters<br />supported only with enterprise version of [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/#ip-filters) |
 | load_balancing_policy<a href="#vmauthunauthorizeduseraccessspec-load_balancing_policy" id="vmauthunauthorizeduseraccessspec-load_balancing_policy">#</a><br/>_string_ | _(Optional)_<br/>LoadBalancingPolicy defines load balancing policy to use for backend urls.<br />Supported policies: least_loaded, first_available.<br />See [here](https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing) for more details (default "least_loaded") |
@@ -4475,7 +4504,7 @@ Appears in: [VMClusterSpec](#vmclusterspec)
 | minReadySeconds<a href="#vmselect-minreadyseconds" id="vmselect-minreadyseconds">#</a><br/>_integer_ | _(Optional)_<br/>MinReadySeconds defines a minimum number of seconds to wait before starting update next pod<br />if previous in healthy state<br />Has no effect for VLogs and VMSingle |
 | nodeSelector<a href="#vmselect-nodeselector" id="vmselect-nodeselector">#</a><br/>_object (keys:string, values:string)_ | _(Optional)_<br/>NodeSelector Define which Nodes the Pods are scheduled on. |
 | paused<a href="#vmselect-paused" id="vmselect-paused">#</a><br/>_boolean_ | _(Optional)_<br/>Paused If set to true all actions on the underlying managed objects are not<br />going to be performed, except for delete actions. |
-| persistentVolume<a href="#vmselect-persistentvolume" id="vmselect-persistentvolume">#</a><br/>_[StorageSpec](#storagespec)_ | _(Optional)_<br/>Storage - add persistent volume for cacheMountPath<br />its useful for persistent cache<br />use storage instead of persistentVolume. |
+| persistentVolume<a href="#vmselect-persistentvolume" id="vmselect-persistentvolume">#</a><br/>_[StorageSpec](#storagespec)_ | _(Optional)_<br/>PersistentVolume - add persistent volume for cacheMountPath<br />its useful for persistent cache<br />use storage instead of persistentVolume. |
 | persistentVolumeClaimRetentionPolicy<a href="#vmselect-persistentvolumeclaimretentionpolicy" id="vmselect-persistentvolumeclaimretentionpolicy">#</a><br/>_[StatefulSetPersistentVolumeClaimRetentionPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.30/#statefulsetpersistentvolumeclaimretentionpolicy-v1-apps)_ | _(Optional)_<br/>PersistentVolumeClaimRetentionPolicy allows configuration of PVC retention policy |
 | podDisruptionBudget<a href="#vmselect-poddisruptionbudget" id="vmselect-poddisruptionbudget">#</a><br/>_[EmbeddedPodDisruptionBudgetSpec](#embeddedpoddisruptionbudgetspec)_ | _(Optional)_<br/>PodDisruptionBudget created by operator |
 | podMetadata<a href="#vmselect-podmetadata" id="vmselect-podmetadata">#</a><br/>_[EmbeddedObjectMetadata](#embeddedobjectmetadata)_ | _(Required)_<br/>PodMetadata configures Labels and Annotations which are propagated to the VMSelect pods. |
@@ -4752,7 +4781,7 @@ Appears in: [VMAuthSpec](#vmauthspec), [VMAuthUnauthorizedUserAccessSpec](#vmaut
 | default_url<a href="#vmuserconfigoptions-default_url" id="vmuserconfigoptions-default_url">#</a><br/>_string array_ | _(Required)_<br/>DefaultURLs backend url for non-matching paths filter<br />usually used for default backend with error message |
 | discover_backend_ips<a href="#vmuserconfigoptions-discover_backend_ips" id="vmuserconfigoptions-discover_backend_ips">#</a><br/>_boolean_ | _(Required)_<br/>DiscoverBackendIPs instructs discovering URLPrefix backend IPs via DNS. |
 | drop_src_path_prefix_parts<a href="#vmuserconfigoptions-drop_src_path_prefix_parts" id="vmuserconfigoptions-drop_src_path_prefix_parts">#</a><br/>_integer_ | _(Optional)_<br/>DropSrcPathPrefixParts is the number of `/`-delimited request path prefix parts to drop before proxying the request to backend.<br />See [here](https://docs.victoriametrics.com/victoriametrics/vmauth/#dropping-request-path-prefix) for more details. |
-| dump_request_on_errors<a href="#vmuserconfigoptions-dump_request_on_errors" id="vmuserconfigoptions-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br />available since v1.107.0 vmauth version |
+| dump_request_on_errors<a href="#vmuserconfigoptions-dump_request_on_errors" id="vmuserconfigoptions-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br /><br />available since v1.107.0 vmauth version |
 | headers<a href="#vmuserconfigoptions-headers" id="vmuserconfigoptions-headers">#</a><br/>_string array_ | _(Optional)_<br/>Headers represent additional http headers, that vmauth uses<br />in form of ["header_key: header_value"]<br />multiple values for header key:<br />["header_key: value1,value2"]<br />it's available since 1.68.0 version of vmauth |
 | ip_filters<a href="#vmuserconfigoptions-ip_filters" id="vmuserconfigoptions-ip_filters">#</a><br/>_[VMUserIPFilters](#vmuseripfilters)_ | _(Optional)_<br/>IPFilters defines per target src ip filters<br />supported only with enterprise version of [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/#ip-filters) |
 | load_balancing_policy<a href="#vmuserconfigoptions-load_balancing_policy" id="vmuserconfigoptions-load_balancing_policy">#</a><br/>_string_ | _(Optional)_<br/>LoadBalancingPolicy defines load balancing policy to use for backend urls.<br />Supported policies: least_loaded, first_available.<br />See [here](https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing) for more details (default "least_loaded") |
@@ -4792,7 +4821,7 @@ Appears in: [VMUser](#vmuser)
 | disable_secret_creation<a href="#vmuserspec-disable_secret_creation" id="vmuserspec-disable_secret_creation">#</a><br/>_boolean_ | _(Required)_<br/>DisableSecretCreation skips related secret creation for vmuser |
 | discover_backend_ips<a href="#vmuserspec-discover_backend_ips" id="vmuserspec-discover_backend_ips">#</a><br/>_boolean_ | _(Required)_<br/>DiscoverBackendIPs instructs discovering URLPrefix backend IPs via DNS. |
 | drop_src_path_prefix_parts<a href="#vmuserspec-drop_src_path_prefix_parts" id="vmuserspec-drop_src_path_prefix_parts">#</a><br/>_integer_ | _(Optional)_<br/>DropSrcPathPrefixParts is the number of `/`-delimited request path prefix parts to drop before proxying the request to backend.<br />See [here](https://docs.victoriametrics.com/victoriametrics/vmauth/#dropping-request-path-prefix) for more details. |
-| dump_request_on_errors<a href="#vmuserspec-dump_request_on_errors" id="vmuserspec-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br />available since v1.107.0 vmauth version |
+| dump_request_on_errors<a href="#vmuserspec-dump_request_on_errors" id="vmuserspec-dump_request_on_errors">#</a><br/>_boolean_ | _(Optional)_<br/>DumpRequestOnErrors instructs vmauth to return detailed request params to the client<br />if routing rules don't allow to forward request to the backends.<br />Useful for debugging `src_hosts` and `src_headers` based routing rules<br /><br />available since v1.107.0 vmauth version |
 | generatePassword<a href="#vmuserspec-generatepassword" id="vmuserspec-generatepassword">#</a><br/>_boolean_ | _(Optional)_<br/>GeneratePassword instructs operator to generate password for user<br />if spec.password if empty. |
 | headers<a href="#vmuserspec-headers" id="vmuserspec-headers">#</a><br/>_string array_ | _(Optional)_<br/>Headers represent additional http headers, that vmauth uses<br />in form of ["header_key: header_value"]<br />multiple values for header key:<br />["header_key: value1,value2"]<br />it's available since 1.68.0 version of vmauth |
 | ip_filters<a href="#vmuserspec-ip_filters" id="vmuserspec-ip_filters">#</a><br/>_[VMUserIPFilters](#vmuseripfilters)_ | _(Optional)_<br/>IPFilters defines per target src ip filters<br />supported only with enterprise version of [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/#ip-filters) |
diff --git a/docs/quick-start.md b/docs/quick-start.md
index 05c527ba..f6926cfc 100644
--- a/docs/quick-start.md
+++ b/docs/quick-start.md
@@ -350,7 +350,7 @@ spec:
     matchLabels:
       app.kubernetes.io/name: demo-app
   endpoints:
-  - port: metrics
+  - port: http
 EOF
 
 kubectl apply -f demo-app-scrape.yaml;
diff --git a/internal/controller/operator/converter/apis.go b/internal/controller/operator/converter/apis.go
index c8571d4e..8bee5d74 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/build/build.go b/internal/controller/operator/factory/build/build.go
index a73baa26..cb6c78ad 100644
--- a/internal/controller/operator/factory/build/build.go
+++ b/internal/controller/operator/factory/build/build.go
@@ -37,9 +37,9 @@ type builderOpts interface {
 func PodDNSAddress(baseName string, podIndex int32, namespace string, portName string, domain string) string {
 	// The default DNS search path is .svc.<cluster domain>
 	if domain == "" {
-		return fmt.Sprintf("%s-%d.%s.%s:%s,", baseName, podIndex, baseName, namespace, portName)
+		return fmt.Sprintf("%s-%d.%s.%s:%s", baseName, podIndex, baseName, namespace, portName)
 	}
-	return fmt.Sprintf("%s-%d.%s.%s.svc.%s:%s,", baseName, podIndex, baseName, namespace, domain, portName)
+	return fmt.Sprintf("%s-%d.%s.%s.svc.%s:%s", baseName, podIndex, baseName, namespace, domain, portName)
 }
 
 // LicenseArgsTo conditionally adds license commandline args into given args
diff --git a/internal/controller/operator/factory/reconcile/service.go b/internal/controller/operator/factory/reconcile/service.go
index 5f69cbe6..d010452a 100644
--- a/internal/controller/operator/factory/reconcile/service.go
+++ b/internal/controller/operator/factory/reconcile/service.go
@@ -31,14 +31,6 @@ func Service(ctx context.Context, rclient client.Client, newService, prevService
 }
 
 func reconcileService(ctx context.Context, rclient client.Client, newService, prevService *corev1.Service) error {
-	var isPrevServiceEqual bool
-	var prevSpecDiff string
-	if prevService != nil {
-		isPrevServiceEqual = equality.Semantic.DeepDerivative(prevService, newService)
-		if !isPrevServiceEqual {
-			prevSpecDiff = diffDeepDerivative(prevService, newService)
-		}
-	}
 	// helper for proper service deletion.
 	recreateService := func(svc *corev1.Service) error {
 		if err := finalize.RemoveFinalizer(ctx, rclient, svc); err != nil {
@@ -69,6 +61,19 @@ func reconcileService(ctx context.Context, rclient client.Client, newService, pr
 	if err := finalize.FreeIfNeeded(ctx, rclient, currentService); err != nil {
 		return err
 	}
+	var isPrevServiceEqual bool
+	var prevSpecDiff string
+	if prevService != nil {
+		isPrevServiceEqual = equality.Semantic.DeepDerivative(prevService, newService)
+		if !isPrevServiceEqual {
+			prevSpecDiff = diffDeepDerivative(prevService, newService)
+		}
+		// keep LoadBalancerClass assigned by cloud-controller
+		// See https://github.com/VictoriaMetrics/operator/issues/1550
+		if prevService.Spec.LoadBalancerClass == nil && newService.Spec.LoadBalancerClass == nil {
+			newService.Spec.LoadBalancerClass = currentService.Spec.LoadBalancerClass
+		}
+	}
 	// invariants
 	switch {
 	case newService.Spec.Type != currentService.Spec.Type:
diff --git a/internal/controller/operator/factory/vlcluster/vlselect.go b/internal/controller/operator/factory/vlcluster/vlselect.go
index 8e780f4e..bda40b2c 100644
--- a/internal/controller/operator/factory/vlcluster/vlselect.go
+++ b/internal/controller/operator/factory/vlcluster/vlselect.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"path"
 	"sort"
-	"strings"
 
 	appsv1 "k8s.io/api/apps/v1"
 	autoscalingv2 "k8s.io/api/autoscaling/v2"
@@ -209,13 +208,19 @@ func buildVLSelectPodSpec(cr *vmv1.VLCluster) (*corev1.PodTemplateSpec, error) {
 
 	if cr.Spec.VLStorage != nil && cr.Spec.VLStorage.ReplicaCount != nil {
 		// TODO: check TLS
-		storageArg := "-storageNode="
-		for _, i := range cr.AvailableStorageNodeIDs("select") {
-			storageArg += build.PodDNSAddress(cr.GetVLStorageName(), i, cr.Namespace, cr.Spec.VLStorage.Port, cr.Spec.ClusterDomainName)
+		storageNodeFlag := build.NewFlag("-storageNode", "")
+		storageNodeIds := cr.AvailableStorageNodeIDs("select")
+		for idx, i := range storageNodeIds {
+			storageNodeFlag.Add(build.PodDNSAddress(cr.GetVLStorageName(), i, cr.Namespace, cr.Spec.VLStorage.Port, cr.Spec.ClusterDomainName), idx)
 		}
-		storageArg = strings.TrimSuffix(storageArg, ",")
-		args = append(args, storageArg)
-
+		if len(cr.Spec.VLSelect.ExtraStorageNodes) > 0 {
+			for i, node := range cr.Spec.VLSelect.ExtraStorageNodes {
+				idx := i + len(storageNodeIds)
+				storageNodeFlag.Add(node.Addr, idx)
+			}
+		}
+		totalNodes := len(cr.Spec.VLSelect.ExtraStorageNodes) + len(storageNodeIds)
+		args = build.AppendFlagsToArgs(args, totalNodes, storageNodeFlag)
 	}
 
 	if len(cr.Spec.VLSelect.ExtraEnvs) > 0 || len(cr.Spec.VLSelect.ExtraEnvsFrom) > 0 {
diff --git a/internal/controller/operator/factory/vmagent/servicescrape.go b/internal/controller/operator/factory/vmagent/servicescrape.go
index 355a22df..21c2735a 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/vmagent_scrapeconfig.go b/internal/controller/operator/factory/vmagent/vmagent_scrapeconfig.go
index 958585a3..472fea4b 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{}
+}
diff --git a/internal/controller/operator/factory/vmcluster/vmcluster.go b/internal/controller/operator/factory/vmcluster/vmcluster.go
index 720ce803..fe15d375 100644
--- a/internal/controller/operator/factory/vmcluster/vmcluster.go
+++ b/internal/controller/operator/factory/vmcluster/vmcluster.go
@@ -522,7 +522,7 @@ func genVMSelectSpec(cr *vmv1beta1.VMCluster) (*appsv1.StatefulSet, error) {
 	}
 	build.StatefulSetAddCommonParams(stsSpec, ptr.Deref(cr.Spec.VMSelect.UseStrictSecurity, false), &cr.Spec.VMSelect.CommonApplicationDeploymentParams)
 	if cr.Spec.VMSelect.CacheMountPath != "" {
-		storageSpec := cr.Spec.VMSelect.Storage
+		storageSpec := cr.Spec.VMSelect.PersistentVolume
 		// hack, storage is deprecated.
 		if storageSpec == nil && cr.Spec.VMSelect.StorageSpec != nil {
 			storageSpec = cr.Spec.VMSelect.StorageSpec
@@ -566,25 +566,23 @@ func makePodSpecForVMSelect(cr *vmv1beta1.VMCluster) (*corev1.PodTemplateSpec, e
 	}
 
 	if cr.Spec.VMStorage != nil && cr.Spec.VMStorage.ReplicaCount != nil {
-
-		storageArg := "-storageNode="
-		for _, i := range cr.AvailableStorageNodeIDs("select") {
-			storageArg += build.PodDNSAddress(cr.GetVMStorageName(), i, cr.Namespace, cr.Spec.VMStorage.VMSelectPort, cr.Spec.ClusterDomainName)
+		storageNodeFlag := build.NewFlag("-storageNode", "")
+		storageNodeIds := cr.AvailableStorageNodeIDs("select")
+		for idx, i := range storageNodeIds {
+			storageNodeFlag.Add(build.PodDNSAddress(cr.GetVMStorageName(), i, cr.Namespace, cr.Spec.VMStorage.VMSelectPort, cr.Spec.ClusterDomainName), idx)
 		}
-		storageArg = strings.TrimSuffix(storageArg, ",")
-		args = append(args, storageArg)
-
+		totalNodes := len(storageNodeIds)
+		args = build.AppendFlagsToArgs(args, totalNodes, storageNodeFlag)
 	}
 	// selectNode arg add for deployments without HPA
 	// HPA leads to rolling restart for vmselect statefulset in case of replicas count changes
 	if cr.Spec.VMSelect.HPA == nil && cr.Spec.VMSelect.ReplicaCount != nil {
-		selectArg := "-selectNode="
+		selectNodeFlag := build.NewFlag("-selectNode", "")
 		vmselectCount := *cr.Spec.VMSelect.ReplicaCount
-		for i := int32(0); i < vmselectCount; i++ {
-			selectArg += build.PodDNSAddress(cr.GetVMSelectName(), i, cr.Namespace, cr.Spec.VMSelect.Port, cr.Spec.ClusterDomainName)
+		for idx := int32(0); idx < vmselectCount; idx++ {
+			selectNodeFlag.Add(build.PodDNSAddress(cr.GetVMSelectName(), idx, cr.Namespace, cr.Spec.VMSelect.Port, cr.Spec.ClusterDomainName), int(idx))
 		}
-		selectArg = strings.TrimSuffix(selectArg, ",")
-		args = append(args, selectArg)
+		args = build.AppendFlagsToArgs(args, int(vmselectCount), selectNodeFlag)
 	}
 
 	if len(cr.Spec.VMSelect.ExtraEnvs) > 0 || len(cr.Spec.VMSelect.ExtraEnvsFrom) > 0 {
@@ -768,15 +766,15 @@ func makePodSpecForVMInsert(cr *vmv1beta1.VMCluster) (*corev1.PodTemplateSpec, e
 	}
 
 	if cr.Spec.VMStorage != nil && cr.Spec.VMStorage.ReplicaCount != nil {
-		storageArg := "-storageNode="
-		for _, i := range cr.AvailableStorageNodeIDs("insert") {
-			storageArg += build.PodDNSAddress(cr.GetVMStorageName(), i, cr.Namespace, cr.Spec.VMStorage.VMInsertPort, cr.Spec.ClusterDomainName)
+		storageNodeFlag := build.NewFlag("-storageNode", "")
+		storageNodeIds := cr.AvailableStorageNodeIDs("insert")
+		for idx, i := range storageNodeIds {
+			storageNodeFlag.Add(build.PodDNSAddress(cr.GetVMStorageName(), i, cr.Namespace, cr.Spec.VMStorage.VMInsertPort, cr.Spec.ClusterDomainName), idx)
 		}
-		storageArg = strings.TrimSuffix(storageArg, ",")
-
-		args = append(args, storageArg)
-
+		totalNodes := len(storageNodeIds)
+		args = build.AppendFlagsToArgs(args, totalNodes, storageNodeFlag)
 	}
+
 	if cr.Spec.ReplicationFactor != nil {
 		args = append(args, fmt.Sprintf("-replicationFactor=%d", *cr.Spec.ReplicationFactor))
 	}
diff --git a/internal/controller/operator/factory/vtcluster/insert.go b/internal/controller/operator/factory/vtcluster/insert.go
index 65d91e8b..f97c7ec5 100644
--- a/internal/controller/operator/factory/vtcluster/insert.go
+++ b/internal/controller/operator/factory/vtcluster/insert.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"path"
 	"sort"
-	"strings"
 
 	appsv1 "k8s.io/api/apps/v1"
 	autoscalingv2 "k8s.io/api/autoscaling/v2"
@@ -138,14 +137,16 @@ func buildVTInsertPodSpec(cr *vmv1.VTCluster) (*corev1.PodTemplateSpec, error) {
 	}
 
 	if cr.Spec.Storage != nil && cr.Spec.Storage.ReplicaCount != nil {
-		storageArg := "-storageNode="
-		for _, i := range cr.AvailableStorageNodeIDs("insert") {
-			// TODO: introduce TLS webserver config for storage nodes
-			storageArg += build.PodDNSAddress(cr.GetVTStorageName(), i, cr.Namespace, cr.Spec.Storage.Port, cr.Spec.ClusterDomainName)
+		// TODO: check TLS
+		storageNodeFlag := build.NewFlag("-storageNode", "")
+		storageNodeIds := cr.AvailableStorageNodeIDs("insert")
+		for idx, i := range storageNodeIds {
+			storageNodeFlag.Add(build.PodDNSAddress(cr.GetVTStorageName(), i, cr.Namespace, cr.Spec.Storage.Port, cr.Spec.ClusterDomainName), idx)
 		}
-		storageArg = strings.TrimSuffix(storageArg, ",")
-		args = append(args, storageArg)
+		totalNodes := len(storageNodeIds)
+		args = build.AppendFlagsToArgs(args, totalNodes, storageNodeFlag)
 	}
+
 	if len(cr.Spec.Insert.ExtraEnvs) > 0 || len(cr.Spec.Insert.ExtraEnvsFrom) > 0 {
 		args = append(args, "-envflag.enable=true")
 	}
diff --git a/internal/controller/operator/factory/vtcluster/select.go b/internal/controller/operator/factory/vtcluster/select.go
index f4eb7077..bc4f3000 100644
--- a/internal/controller/operator/factory/vtcluster/select.go
+++ b/internal/controller/operator/factory/vtcluster/select.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"path"
 	"sort"
-	"strings"
 
 	appsv1 "k8s.io/api/apps/v1"
 	autoscalingv2 "k8s.io/api/autoscaling/v2"
@@ -209,13 +208,19 @@ func buildVTSelectPodSpec(cr *vmv1.VTCluster) (*corev1.PodTemplateSpec, error) {
 
 	if cr.Spec.Storage != nil && cr.Spec.Storage.ReplicaCount != nil {
 		// TODO: check TLS
-		storageArg := "-storageNode="
-		for _, i := range cr.AvailableStorageNodeIDs("select") {
-			storageArg += build.PodDNSAddress(cr.GetVTStorageName(), i, cr.Namespace, cr.Spec.Storage.Port, cr.Spec.ClusterDomainName)
+		storageNodeFlag := build.NewFlag("-storageNode", "")
+		storageNodeIds := cr.AvailableStorageNodeIDs("select")
+		for idx, i := range storageNodeIds {
+			storageNodeFlag.Add(build.PodDNSAddress(cr.GetVTStorageName(), i, cr.Namespace, cr.Spec.Storage.Port, cr.Spec.ClusterDomainName), idx)
 		}
-		storageArg = strings.TrimSuffix(storageArg, ",")
-		args = append(args, storageArg)
-
+		if len(cr.Spec.Select.ExtraStorageNodes) > 0 {
+			for i, node := range cr.Spec.Select.ExtraStorageNodes {
+				idx := i + len(storageNodeIds)
+				storageNodeFlag.Add(node.Addr, idx)
+			}
+		}
+		totalNodes := len(cr.Spec.Select.ExtraStorageNodes) + len(storageNodeIds)
+		args = build.AppendFlagsToArgs(args, totalNodes, storageNodeFlag)
 	}
 
 	if len(cr.Spec.Select.ExtraEnvs) > 0 || len(cr.Spec.Select.ExtraEnvsFrom) > 0 {
diff --git a/internal/webhook/operator/v1beta1/vmcluster_webhook.go b/internal/webhook/operator/v1beta1/vmcluster_webhook.go
index ff97a6c4..ed26471d 100644
--- a/internal/webhook/operator/v1beta1/vmcluster_webhook.go
+++ b/internal/webhook/operator/v1beta1/vmcluster_webhook.go
@@ -60,7 +60,7 @@ func (*VMClusterCustomValidator) ValidateCreate(ctx context.Context, obj runtime
 		warnings = append(warnings, "deprecated property is defined `spec.vmstorage.vmbackup.acceptEula`, use `spec.license.key` or `spec.license.keyRef` instead.")
 		logger.WithContext(ctx).Info("deprecated property is defined `spec.vmstorage.vmbackup.acceptEula`, use `spec.license.key` or `spec.license.keyRef` instead.")
 	}
-	if r.Spec.VMSelect != nil && r.Spec.VMSelect.StorageSpec != nil {
+	if r.Spec.VMSelect != nil && r.Spec.VMSelect.PersistentVolume != nil {
 		warnings = append(warnings, "deprecated property is defined `spec.vmselect.persistentVolume`, use `storage` instead.")
 		logger.WithContext(ctx).Info("deprecated property is defined `spec.vmselect.persistentVolume`, use `storage` instead.")
 	}
@@ -85,7 +85,7 @@ func (*VMClusterCustomValidator) ValidateUpdate(ctx context.Context, _, newObj r
 		warnings = append(warnings, "deprecated property is defined `spec.vmbackup.acceptEula`, use `spec.license.key` or `spec.license.keyRef` instead.")
 		logger.WithContext(ctx).Info("deprecated property is defined `spec.vmbackup.acceptEula`, use `spec.license.key` or `spec.license.keyRef` instead.")
 	}
-	if r.Spec.VMSelect != nil && r.Spec.VMSelect.StorageSpec != nil {
+	if r.Spec.VMSelect != nil && r.Spec.VMSelect.PersistentVolume != nil {
 		warnings = append(warnings, "deprecated property is defined `vmcluster.spec.vmselect.persistentVolume`, use `storage` instead.")
 		logger.WithContext(ctx).Info("deprecated property is defined `vmcluster.spec.vmselect.persistentVolume`, use `storage` instead.")
 	}



Test patch:
diff --git a/internal/controller/operator/factory/reconcile/service_test.go b/internal/controller/operator/factory/reconcile/service_test.go
index f67a4a7e..32fc100f 100644
--- a/internal/controller/operator/factory/reconcile/service_test.go
+++ b/internal/controller/operator/factory/reconcile/service_test.go
@@ -17,20 +17,22 @@ import (
 func Test_reconcileServiceForCRD(t *testing.T) {
 
 	type opts struct {
-		serviceToReconcile *corev1.Service
-		predefinedObjects  []runtime.Object
-		validate           func(svc *corev1.Service) error
+		newService        *corev1.Service
+		prevService       *corev1.Service
+		predefinedObjects []runtime.Object
+		validate          func(svc *corev1.Service) error
 	}
+
 	f := func(opts opts) {
 		t.Helper()
 		cl := k8stools.GetTestClientWithObjects(opts.predefinedObjects)
 		ctx := context.Background()
-		err := Service(ctx, cl, opts.serviceToReconcile, nil)
+		err := Service(ctx, cl, opts.newService, opts.prevService)
 		if err != nil {
 			t.Fatalf("unexpected reconcileServiceForCRD() error = %s", err)
 		}
 		var gotSvc corev1.Service
-		if err := cl.Get(ctx, types.NamespacedName{Namespace: opts.serviceToReconcile.Namespace, Name: opts.serviceToReconcile.Name}, &gotSvc); err != nil {
+		if err := cl.Get(ctx, types.NamespacedName{Namespace: opts.newService.Namespace, Name: opts.newService.Name}, &gotSvc); err != nil {
 			t.Fatalf("unexpected error: %v", err)
 		}
 		if err := opts.validate(&gotSvc); err != nil {
@@ -39,7 +41,7 @@ func Test_reconcileServiceForCRD(t *testing.T) {
 	}
 
 	f(opts{
-		serviceToReconcile: &corev1.Service{
+		newService: &corev1.Service{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "prefixed-1",
 				Namespace: "default",
@@ -58,7 +60,7 @@ func Test_reconcileServiceForCRD(t *testing.T) {
 
 	// update loadbalancer class
 	f(opts{
-		serviceToReconcile: &corev1.Service{
+		newService: &corev1.Service{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "prefixed-1",
 				Namespace: "default",
@@ -87,281 +89,285 @@ func Test_reconcileServiceForCRD(t *testing.T) {
 		},
 	})
 
-	// TODO: migrate other tests for f-test
-	type args struct {
-		ctx        context.Context
-		newService *corev1.Service
-	}
-	tests := []struct {
-		name              string
-		args              args
-		predefinedObjects []runtime.Object
-		validate          func(svc *corev1.Service) error
-		wantErr           bool
-	}{
-
-		{
-			name: "update svc from headless to clusterIP",
-			args: args{
-				newService: &corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type: corev1.ServiceTypeClusterIP,
-					},
-				},
-				ctx: context.TODO(),
+	// do not recreate loadbalancer class
+	f(opts{
+		newService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
 			},
-			predefinedObjects: []runtime.Object{
-				&corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeClusterIP,
-						ClusterIP: "None",
-					},
-				},
+			Spec: corev1.ServiceSpec{
+				Type: corev1.ServiceTypeLoadBalancer,
 			},
-			validate: func(svc *corev1.Service) error {
-				if svc.Name != "prefixed-1" {
-					return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
-				}
-				if svc.Spec.ClusterIP == "None" {
-					return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
-				}
-				return nil
+		},
+		prevService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
+			},
+			Spec: corev1.ServiceSpec{
+				Type: corev1.ServiceTypeLoadBalancer,
 			},
 		},
-		{
-			name: "update svc from clusterIP to headless",
-			args: args{
-				newService: &corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeClusterIP,
-						ClusterIP: "None",
-					},
+		predefinedObjects: []runtime.Object{
+			&corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "prefixed-1",
+					Namespace: "default",
+					Labels:    map[string]string{"custom": "label"},
 				},
-				ctx: context.TODO(),
-			},
-			predefinedObjects: []runtime.Object{
-				&corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeClusterIP,
-						ClusterIP: "192.168.1.5",
-					},
+				Spec: corev1.ServiceSpec{
+					Type:              corev1.ServiceTypeLoadBalancer,
+					LoadBalancerClass: ptr.To("some-class"),
 				},
 			},
-			validate: func(svc *corev1.Service) error {
-				if svc.Name != "prefixed-1" {
-					return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
-				}
-				if svc.Spec.ClusterIP != "None" {
-					return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
-				}
-				return nil
+		},
+		validate: func(svc *corev1.Service) error {
+			if *svc.Spec.LoadBalancerClass != "some-class" {
+				return fmt.Errorf("unexpected LoadBalancerClass: %s, want: some-classs", *svc.Spec.LoadBalancerClass)
+			}
+			l, ok := svc.Labels["custom"]
+			if !ok {
+				return fmt.Errorf("missing 'custom' label on svc")
+			}
+			if l != "label" {
+				return fmt.Errorf("unexpected value of 'custom' label on svc, got: %v, want: 'value'", l)
+			}
+			return nil
+		},
+	})
+
+	// update svc from headless to clusterIP
+	f(opts{
+		newService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
+			},
+			Spec: corev1.ServiceSpec{
+				Type: corev1.ServiceTypeClusterIP,
 			},
 		},
-		{
-			name: "update svc clusterIP value",
-			args: args{
-				newService: &corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeClusterIP,
-						ClusterIP: "192.168.1.5",
-					},
+		predefinedObjects: []runtime.Object{
+			&corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "prefixed-1",
+					Namespace: "default",
 				},
-				ctx: context.TODO(),
-			},
-			predefinedObjects: []runtime.Object{
-				&corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeClusterIP,
-						ClusterIP: "192.168.1.4",
-					},
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeClusterIP,
+					ClusterIP: "None",
 				},
 			},
-			validate: func(svc *corev1.Service) error {
-				if svc.Name != "prefixed-1" {
-					return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
-				}
-				if svc.Spec.ClusterIP != "192.168.1.5" {
-					return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
-				}
-				return nil
+		},
+		validate: func(svc *corev1.Service) error {
+			if svc.Name != "prefixed-1" {
+				return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
+			}
+			if svc.Spec.ClusterIP == "None" {
+				return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
+			}
+			return nil
+		},
+	})
+
+	// update svc from clusterIP to headless
+	f(opts{
+		newService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
+			},
+			Spec: corev1.ServiceSpec{
+				Type:      corev1.ServiceTypeClusterIP,
+				ClusterIP: "None",
 			},
 		},
-		{
-			name: "update svc from nodePort to clusterIP with value",
-			args: args{
-				newService: &corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeClusterIP,
-						ClusterIP: "192.168.1.5",
-					},
+		predefinedObjects: []runtime.Object{
+			&corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "prefixed-1",
+					Namespace: "default",
+				},
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeClusterIP,
+					ClusterIP: "192.168.1.5",
 				},
-				ctx: context.TODO(),
 			},
-			predefinedObjects: []runtime.Object{
-				&corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeNodePort,
-						ClusterIP: "192.168.1.1",
-					},
+		},
+		validate: func(svc *corev1.Service) error {
+			if svc.Name != "prefixed-1" {
+				return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
+			}
+			if svc.Spec.ClusterIP != "None" {
+				return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
+			}
+			return nil
+		},
+	})
+
+	// update svc clusterIP value
+	f(opts{
+		newService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
+			},
+			Spec: corev1.ServiceSpec{
+				Type:      corev1.ServiceTypeClusterIP,
+				ClusterIP: "192.168.1.5",
+			},
+		},
+		predefinedObjects: []runtime.Object{
+			&corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "prefixed-1",
+					Namespace: "default",
 				},
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeClusterIP,
+					ClusterIP: "192.168.1.4",
+				},
+			},
+		},
+		validate: func(svc *corev1.Service) error {
+			if svc.Name != "prefixed-1" {
+				return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
+			}
+			if svc.Spec.ClusterIP != "192.168.1.5" {
+				return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
+			}
+			return nil
+		},
+	})
+
+	// update svc from nodePort to clusterIP with value
+	f(opts{
+		newService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
 			},
-			validate: func(svc *corev1.Service) error {
-				if svc.Name != "prefixed-1" {
-					return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
-				}
-				if svc.Spec.Type != corev1.ServiceTypeClusterIP {
-					return fmt.Errorf("unexpected type: %v", svc.Spec.Type)
-				}
-				if svc.Spec.ClusterIP != "192.168.1.5" {
-					return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
-				}
-				return nil
+			Spec: corev1.ServiceSpec{
+				Type:      corev1.ServiceTypeClusterIP,
+				ClusterIP: "192.168.1.5",
 			},
 		},
-		{
-			name: "keep node port",
-			args: args{
-				newService: &corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeNodePort,
-						ClusterIP: "192.168.1.5",
-						Ports: []corev1.ServicePort{
-							{
-								Name:     "web",
-								Protocol: "TCP",
-							},
-						},
-					},
+		predefinedObjects: []runtime.Object{
+			&corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "prefixed-1",
+					Namespace: "default",
+				},
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeNodePort,
+					ClusterIP: "192.168.1.1",
 				},
-				ctx: context.TODO(),
 			},
-			predefinedObjects: []runtime.Object{
-				&corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
+		},
+		validate: func(svc *corev1.Service) error {
+			if svc.Name != "prefixed-1" {
+				return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
+			}
+			if svc.Spec.Type != corev1.ServiceTypeClusterIP {
+				return fmt.Errorf("unexpected type: %v", svc.Spec.Type)
+			}
+			if svc.Spec.ClusterIP != "192.168.1.5" {
+				return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
+			}
+			return nil
+		},
+	})
+
+	// keep node port
+	f(opts{
+		newService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
+			},
+			Spec: corev1.ServiceSpec{
+				Type:      corev1.ServiceTypeNodePort,
+				ClusterIP: "192.168.1.5",
+				Ports: []corev1.ServicePort{
+					{
+						Name:     "web",
+						Protocol: "TCP",
 					},
-					Spec: corev1.ServiceSpec{
-						Type:      corev1.ServiceTypeNodePort,
-						ClusterIP: "192.168.1.5",
-						Ports: []corev1.ServicePort{
-							{
-								Name:     "web",
-								Protocol: "TCP",
-								NodePort: 331,
-							},
+				},
+			},
+		},
+		predefinedObjects: []runtime.Object{
+			&corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "prefixed-1",
+					Namespace: "default",
+				},
+				Spec: corev1.ServiceSpec{
+					Type:      corev1.ServiceTypeNodePort,
+					ClusterIP: "192.168.1.5",
+					Ports: []corev1.ServicePort{
+						{
+							Name:     "web",
+							Protocol: "TCP",
+							NodePort: 331,
 						},
 					},
 				},
 			},
-			validate: func(svc *corev1.Service) error {
-				if svc.Name != "prefixed-1" {
-					return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
-				}
-				if svc.Spec.Type != corev1.ServiceTypeNodePort {
-					return fmt.Errorf("unexpected type: %v", svc.Spec.Type)
-				}
-				if svc.Spec.ClusterIP != "192.168.1.5" {
-					return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
-				}
-				if svc.Spec.Ports[0].NodePort != 331 {
-					return fmt.Errorf("unexpected value for node port: %v", svc.Spec.Ports[0])
-				}
-				return nil
+		},
+		validate: func(svc *corev1.Service) error {
+			if svc.Name != "prefixed-1" {
+				return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
+			}
+			if svc.Spec.Type != corev1.ServiceTypeNodePort {
+				return fmt.Errorf("unexpected type: %v", svc.Spec.Type)
+			}
+			if svc.Spec.ClusterIP != "192.168.1.5" {
+				return fmt.Errorf("unexpected value for clusterIP, want ip, got: %v", svc.Spec.ClusterIP)
+			}
+			if svc.Spec.Ports[0].NodePort != 331 {
+				return fmt.Errorf("unexpected value for node port: %v", svc.Spec.Ports[0])
+			}
+			return nil
+		},
+	})
+
+	// keep custom labels on svc
+	f(opts{
+		newService: &corev1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "prefixed-1",
+				Namespace: "default",
+			},
+			Spec: corev1.ServiceSpec{
+				Type: corev1.ServiceTypeClusterIP,
 			},
 		},
-		{
-			name: "keep custom labels on svc",
-			args: args{
-				newService: &corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-					},
-					Spec: corev1.ServiceSpec{
-						Type: corev1.ServiceTypeClusterIP,
-					},
+		predefinedObjects: []runtime.Object{
+			&corev1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "prefixed-1",
+					Namespace: "default",
+					Labels:    map[string]string{"custom": "label"},
 				},
-				ctx: context.TODO(),
-			},
-			predefinedObjects: []runtime.Object{
-				&corev1.Service{
-					ObjectMeta: metav1.ObjectMeta{
-						Name:      "prefixed-1",
-						Namespace: "default",
-						Labels:    map[string]string{"custom": "label"},
-					},
-					Spec: corev1.ServiceSpec{
-						Type: corev1.ServiceTypeClusterIP,
-					},
+				Spec: corev1.ServiceSpec{
+					Type: corev1.ServiceTypeClusterIP,
 				},
 			},
-			validate: func(svc *corev1.Service) error {
-				if svc.Name != "prefixed-1" {
-					return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
-				}
-				l, ok := svc.Labels["custom"]
-				if !ok {
-					return fmt.Errorf("missing 'custom' label on svc")
-				}
-				if l != "label" {
-					return fmt.Errorf("unexpected value of 'custom' label on svc, got: %v, want: 'value'", l)
-				}
-				return nil
-			},
 		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			cl := k8stools.GetTestClientWithObjects(tt.predefinedObjects)
-			err := Service(tt.args.ctx, cl, tt.args.newService, nil)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("reconcileServiceForCRD() error = %v, wantErr %v", err, tt.wantErr)
-				return
+		validate: func(svc *corev1.Service) error {
+			if svc.Name != "prefixed-1" {
+				return fmt.Errorf("unexpected name, got: %v, want: prefixed-1", svc.Name)
 			}
-			var updatedSvc corev1.Service
-			if err := cl.Get(tt.args.ctx, types.NamespacedName{Namespace: tt.args.newService.Namespace, Name: tt.args.newService.Name}, &updatedSvc); err != nil {
-				t.Fatalf("unexpected error: %v", err)
+			l, ok := svc.Labels["custom"]
+			if !ok {
+				return fmt.Errorf("missing 'custom' label on svc")
 			}
-			if err := tt.validate(&updatedSvc); err != nil {
-				t.Errorf("reconcileServiceForCRD() unexpected error: %v.", err)
+			if l != "label" {
+				return fmt.Errorf("unexpected value of 'custom' label on svc, got: %v, want: 'value'", l)
 			}
-		})
-	}
+			return nil
+		},
+	})
 }
diff --git a/internal/controller/operator/factory/vlcluster/vlcluster_test.go b/internal/controller/operator/factory/vlcluster/vlcluster_test.go
index 2332d427..da684939 100644
--- a/internal/controller/operator/factory/vlcluster/vlcluster_test.go
+++ b/internal/controller/operator/factory/vlcluster/vlcluster_test.go
@@ -237,7 +237,48 @@ func TestCreateOrUpdate(t *testing.T) {
 		return nil
 	}
 
-	// base cluster
+	f(cr, validate, nil)
+
+	// with extra read-only storages
+	cr = &vmv1.VLCluster{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "read-only",
+			Namespace: "default",
+		},
+		Spec: vmv1.VLClusterSpec{
+			VLSelect: &vmv1.VLSelect{
+				ExtraStorageNodes: []vmv1.VLStorageNode{
+					{
+						Addr: "localhost:10101",
+					},
+				},
+				CommonApplicationDeploymentParams: vmv1beta1.CommonApplicationDeploymentParams{
+					ReplicaCount: ptr.To(int32(1)),
+				},
+			},
+			VLStorage: &vmv1.VLStorage{
+				RetentionPeriod: "1w",
+				CommonApplicationDeploymentParams: vmv1beta1.CommonApplicationDeploymentParams{
+					ReplicaCount: ptr.To(int32(1)),
+				},
+			},
+		},
+	}
+	validate = func(ctx context.Context, rclient client.Client) error {
+
+		// check select
+		var d appsv1.Deployment
+		assert.Nil(t, rclient.Get(ctx, types.NamespacedName{Name: cr.GetVLSelectName(), Namespace: cr.Namespace}, &d))
+		assert.Len(t, d.Spec.Template.Spec.Containers, 1)
+		cnt := d.Spec.Template.Spec.Containers[0]
+		assert.Equal(t, cnt.Args, []string{
+			"-httpListenAddr=:9471",
+			"-internalinsert.disable=true",
+			"-storageNode=vlstorage-read-only-0.vlstorage-read-only.default:9491,localhost:10101",
+		})
+		return nil
+	}
+
 	f(cr, validate, nil)
 
 }
diff --git a/internal/controller/operator/factory/vmagent/servicescrape_test.go b/internal/controller/operator/factory/vmagent/servicescrape_test.go
index af3d2855..9ed0f579 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
 `,
 		},
 	}