diff --git a/api/operator/v1alpha1/vmdistributed_types.go b/api/operator/v1alpha1/vmdistributed_types.go index e6669b2e8..55760fc0b 100644 --- a/api/operator/v1alpha1/vmdistributed_types.go +++ b/api/operator/v1alpha1/vmdistributed_types.go @@ -84,11 +84,23 @@ type VMDistributedZoneCommon struct { UpdatePause *metav1.Duration `json:"updatePause,omitempty"` } +type VMDistributedTrafficMode string + +const ( + VMDistributedTrafficModeReadWrite VMDistributedTrafficMode = "read-write" + VMDistributedTrafficModeReadOnly VMDistributedTrafficMode = "read-only" + VMDistributedTrafficModeWriteOnly VMDistributedTrafficMode = "write-only" + VMDistributedTrafficModeMaintenance VMDistributedTrafficMode = "maintenance" +) + // +k8s:openapi-gen=true // VMDistributedZone defines items within a single zone to update. type VMDistributedZone struct { // Name defines a name of zone, which can be used in zoneCommon spec as %ZONE% Name string `json:"name"` + // TrafficMode defines allowed traffic mode for a zone: read-only, write-only, read-write, maintenance + // +kubebuilder:validation:Enum=read-only;write-only;read-write;maintenance + TrafficMode VMDistributedTrafficMode `json:"trafficMode,omitempty"` // VMCluster defines a new inline or referencing existing one VMCluster // +optional VMCluster VMDistributedZoneCluster `json:"vmcluster,omitempty"` diff --git a/config/crd/overlay/crd.descriptionless.yaml b/config/crd/overlay/crd.descriptionless.yaml index fbab3c958..76881ae54 100644 --- a/config/crd/overlay/crd.descriptionless.yaml +++ b/config/crd/overlay/crd.descriptionless.yaml @@ -27510,6 +27510,13 @@ spec: type: string remoteWrite: x-kubernetes-preserve-unknown-fields: true + trafficMode: + enum: + - read-only + - write-only + - read-write + - maintenance + type: string vmagent: properties: name: diff --git a/config/crd/overlay/crd.yaml b/config/crd/overlay/crd.yaml index d20af526f..38ebffb0a 100644 --- a/config/crd/overlay/crd.yaml +++ b/config/crd/overlay/crd.yaml @@ -55810,6 +55810,15 @@ spec: description: RemoteWrite defines VMAgent remote write settings for given zone x-kubernetes-preserve-unknown-fields: true + trafficMode: + description: 'TrafficMode defines allowed traffic mode for a + zone: read-only, write-only, read-write, maintenance' + enum: + - read-only + - write-only + - read-write + - maintenance + type: string vmagent: description: VMAgent defines VMAgent to balance incoming traffic between VMClusters. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 53ce0a760..e6a4ee304 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -14,6 +14,7 @@ aliases: ## tip * FEATURE: [vmauth](https://docs.victoriametrics.com/operator/resources/vmauth): previously VMAuth could read configuration only from predefined locations; now VMAuth supports arbitrary filesystem access configuration, allowing users to reference required files directly and reducing configuration workarounds. See [#899](https://github.com/VictoriaMetrics/operator/issues/899). +* FEATURE: [vmdistributed](https://docs.victoriametrics.com/operator/resources/vmdistributed): introduce `spec.zones[*].trafficMode` property, which allows disable read, write or whole traffic to a zone. See [#1995](https://github.com/VictoriaMetrics/operator/issues/1995). * BUGFIX: [converter](https://docs.victoriametrics.com/operator/integrations/prometheus/#objects-conversion): disable all prometheus controllers if CRD group was not found. See [#2838](https://github.com/VictoriaMetrics/helm-charts/issues/2838). diff --git a/docs/api.md b/docs/api.md index 63b7790e5..b3e699e6f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1145,6 +1145,12 @@ Appears in: [VMDistributed](#vmdistributed) | zoneCommon#
_[VMDistributedZoneCommon](#vmdistributedzonecommon)_ | _(Optional)_
ZoneCommon defines common properties for all zones | | zones#
_[VMDistributedZone](#vmdistributedzone) array_ | _(Required)_
Zones is a list of zones to update. Each item in the list represents a "zone" within the distributed setup. | +#### VMDistributedTrafficMode + +_Underlying type:_ _string_ + +Appears in: [VMDistributedZone](#vmdistributedzone) + #### VMDistributedZone VMDistributedZone defines items within a single zone to update. @@ -1155,6 +1161,7 @@ Appears in: [VMDistributedSpec](#vmdistributedspec) | --- | --- | | name#
_string_ | _(Required)_
Name defines a name of zone, which can be used in zoneCommon spec as %ZONE% | | remoteWrite#
_[VMDistributedZoneRemoteWriteSpec](#vmdistributedzoneremotewritespec)_ | _(Optional)_
RemoteWrite defines VMAgent remote write settings for given zone | +| trafficMode#
_[VMDistributedTrafficMode](#vmdistributedtrafficmode)_ | _(Required)_
TrafficMode defines allowed traffic mode for a zone: read-only, write-only, read-write, maintenance | | vmagent#
_[VMDistributedZoneAgent](#vmdistributedzoneagent)_ | _(Optional)_
VMAgent defines VMAgent to balance incoming traffic between VMClusters. | | vmcluster#
_[VMDistributedZoneCluster](#vmdistributedzonecluster)_ | _(Optional)_
VMCluster defines a new inline or referencing existing one VMCluster | diff --git a/internal/controller/operator/factory/build/defaults.go b/internal/controller/operator/factory/build/defaults.go index cc70e86ad..5f30b526d 100644 --- a/internal/controller/operator/factory/build/defaults.go +++ b/internal/controller/operator/factory/build/defaults.go @@ -52,7 +52,6 @@ func AddDefaults(scheme *runtime.Scheme) { func addVMDistributedDefaults(objI any) { cr := objI.(*vmv1alpha1.VMDistributed) - if cr.Spec.ZoneCommon.ReadyTimeout == nil { cr.Spec.ZoneCommon.ReadyTimeout = &metav1.Duration{ Duration: 5 * time.Minute, @@ -63,6 +62,12 @@ func addVMDistributedDefaults(objI any) { Duration: 1 * time.Minute, } } + for i := range cr.Spec.Zones { + z := &cr.Spec.Zones[i] + if len(z.TrafficMode) == 0 { + z.TrafficMode = vmv1alpha1.VMDistributedTrafficModeReadWrite + } + } if cr.Spec.License.IsProvided() { if !cr.Spec.VMAuth.Spec.License.IsProvided() { cr.Spec.VMAuth.Spec.License = cr.Spec.License.DeepCopy() diff --git a/internal/controller/operator/factory/vmdistributed/vmauth.go b/internal/controller/operator/factory/vmdistributed/vmauth.go index 16b7bbf06..70e044b8d 100644 --- a/internal/controller/operator/factory/vmdistributed/vmauth.go +++ b/internal/controller/operator/factory/vmdistributed/vmauth.go @@ -96,7 +96,7 @@ func vmAgentTargetRef(vmAgents []*vmv1beta1.VMAgent, owner *metav1.OwnerReferenc return ref } -func buildVMAuthLB(cr *vmv1alpha1.VMDistributed, vmAgents []*vmv1beta1.VMAgent, vmClusters []*vmv1beta1.VMCluster, excludeIds ...int) *vmv1beta1.VMAuth { +func buildVMAuthLB(cr *vmv1alpha1.VMDistributed, vmAgents []*vmv1beta1.VMAgent, vmClusters []*vmv1beta1.VMCluster, trafficModes []vmv1alpha1.VMDistributedTrafficMode, excludeIds ...int) *vmv1beta1.VMAuth { if !ptr.Deref(cr.Spec.VMAuth.Enabled, true) || build.IsControllerDisabled("VMAuth") { return nil } @@ -108,10 +108,23 @@ func buildVMAuthLB(cr *vmv1alpha1.VMDistributed, vmAgents []*vmv1beta1.VMAgent, }, Spec: *cr.Spec.VMAuth.Spec.DeepCopy(), } + writeExcludeIds := slices.Clone(excludeIds) + readExcludeIds := slices.Clone(excludeIds) + for i, mode := range trafficModes { + switch mode { + case vmv1alpha1.VMDistributedTrafficModeReadOnly: + writeExcludeIds = append(writeExcludeIds, i) + case vmv1alpha1.VMDistributedTrafficModeWriteOnly: + readExcludeIds = append(readExcludeIds, i) + case vmv1alpha1.VMDistributedTrafficModeMaintenance: + writeExcludeIds = append(writeExcludeIds, i) + readExcludeIds = append(readExcludeIds, i) + } + } var targetRefs []vmv1beta1.TargetRef owner := cr.AsOwner() - targetRefs = append(targetRefs, vmAgentTargetRef(vmAgents, &owner, excludeIds...)) - targetRefs = append(targetRefs, vmClusterTargetRef(vmClusters, &owner, excludeIds...)) + targetRefs = append(targetRefs, vmAgentTargetRef(vmAgents, &owner, writeExcludeIds...)) + targetRefs = append(targetRefs, vmClusterTargetRef(vmClusters, &owner, readExcludeIds...)) vmAuth.Spec.DefaultTargetRefs = targetRefs return &vmAuth } diff --git a/internal/controller/operator/factory/vmdistributed/vmauth_test.go b/internal/controller/operator/factory/vmdistributed/vmauth_test.go index e2caf3245..4211b519c 100644 --- a/internal/controller/operator/factory/vmdistributed/vmauth_test.go +++ b/internal/controller/operator/factory/vmdistributed/vmauth_test.go @@ -160,7 +160,7 @@ func TestBuildVMAuthLBZoneOrder(t *testing.T) { f := func(o opts) { t.Helper() - vmAuth := buildVMAuthLB(cr, o.agents, o.clusters, o.excludeIds...) + vmAuth := buildVMAuthLB(cr, o.agents, o.clusters, nil, o.excludeIds...) assert.NotNil(t, vmAuth) assert.Len(t, vmAuth.Spec.DefaultTargetRefs, 2) diff --git a/internal/controller/operator/factory/vmdistributed/vmdistributed_test.go b/internal/controller/operator/factory/vmdistributed/vmdistributed_test.go index df2fcc073..61c3c3102 100644 --- a/internal/controller/operator/factory/vmdistributed/vmdistributed_test.go +++ b/internal/controller/operator/factory/vmdistributed/vmdistributed_test.go @@ -276,7 +276,7 @@ func TestCreateOrUpdate(t *testing.T) { } }, validate: func(ctx context.Context, rclient client.Client, d *testData) { - vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters) + vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters, d.zones.trafficModes) owner := d.cr.AsOwner() assert.NoError(t, reconcile.VMAuth(ctx, rclient, vmAuth, nil, &owner)) }, @@ -291,7 +291,7 @@ func TestCreateOrUpdate(t *testing.T) { }, preRun: func(c client.Client, d *testData) { clusters := []*vmv1beta1.VMCluster{d.zones.vmclusters[0]} - lb := buildVMAuthLB(d.cr, d.zones.vmagents, clusters) + lb := buildVMAuthLB(d.cr, d.zones.vmagents, clusters, nil) c.Scheme().Default(lb) assert.NoError(t, c.Create(context.TODO(), lb)) }, @@ -316,7 +316,7 @@ func TestCreateOrUpdate(t *testing.T) { LogLevel: "INFO", } clusters := []*vmv1beta1.VMCluster{d.zones.vmclusters[0]} - vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, clusters) + vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, clusters, nil) owner := d.cr.AsOwner() assert.NoError(t, reconcile.VMAuth(ctx, rclient, vmAuth, nil, &owner)) }, @@ -333,7 +333,7 @@ func TestCreateOrUpdate(t *testing.T) { }, preRun: func(c client.Client, d *testData) { clusters := []*vmv1beta1.VMCluster{d.zones.vmclusters[0]} - lb := buildVMAuthLB(d.cr, d.zones.vmagents, clusters) + lb := buildVMAuthLB(d.cr, d.zones.vmagents, clusters, nil) c.Scheme().Default(lb) assert.NoError(t, c.Create(context.TODO(), lb)) }, @@ -351,7 +351,7 @@ func TestCreateOrUpdate(t *testing.T) { }, validate: func(ctx context.Context, rclient client.Client, d *testData) { clusters := []*vmv1beta1.VMCluster{d.zones.vmclusters[0]} - vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, clusters) + vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, clusters, nil) owner := d.cr.AsOwner() assert.NoError(t, reconcile.VMAuth(ctx, rclient, vmAuth, nil, &owner)) }, @@ -389,7 +389,7 @@ func TestCreateOrUpdate(t *testing.T) { } }, validate: func(ctx context.Context, rclient client.Client, d *testData) { - vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters) + vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters, d.zones.trafficModes) owner := d.cr.AsOwner() assert.NoError(t, reconcile.VMAuth(ctx, rclient, vmAuth, nil, &owner)) var vmClusterObjs, vmAgentObjs []vmv1beta1.NamespacedName @@ -451,7 +451,7 @@ func TestCreateOrUpdate(t *testing.T) { }, preRun: func(c client.Client, d *testData) { clusters := []*vmv1beta1.VMCluster{d.zones.vmclusters[0]} - lb := buildVMAuthLB(d.cr, d.zones.vmagents, clusters) + lb := buildVMAuthLB(d.cr, d.zones.vmagents, clusters, nil) c.Scheme().Default(lb) lb.OwnerReferences = nil assert.NoError(t, c.Create(context.TODO(), lb)) @@ -478,7 +478,7 @@ func TestCreateOrUpdate(t *testing.T) { }, validate: func(ctx context.Context, rclient client.Client, d *testData) { clusters := []*vmv1beta1.VMCluster{d.zones.vmclusters[0]} - vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, clusters) + vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, clusters, nil) owner := d.cr.AsOwner() assert.NoError(t, reconcile.VMAuth(ctx, rclient, vmAuth, nil, &owner)) var got vmv1beta1.VMAuth @@ -524,7 +524,7 @@ func TestCreateOrUpdate(t *testing.T) { } }, validate: func(ctx context.Context, rclient client.Client, d *testData) { - vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters) + vmAuth := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters, d.zones.trafficModes) owner := d.cr.AsOwner() assert.NoError(t, reconcile.VMAuth(ctx, rclient, vmAuth, nil, &owner)) var vmClusterObjs, vmAgentObjs []vmv1beta1.NamespacedName @@ -582,7 +582,7 @@ func TestCreateOrUpdate(t *testing.T) { }, preRun: func(c client.Client, d *testData) { // Create initial VMAuth backed by CRD targets - lb := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters) + lb := buildVMAuthLB(d.cr, d.zones.vmagents, d.zones.vmclusters, d.zones.trafficModes) c.Scheme().Default(lb) assert.NoError(t, c.Create(context.TODO(), lb)) }, @@ -608,7 +608,7 @@ func TestCreateOrUpdate(t *testing.T) { }, validate: func(ctx context.Context, rclient client.Client, d *testData) { // Simulate removing both backends by passing empty slices - vmAuth := buildVMAuthLB(d.cr, nil, nil) + vmAuth := buildVMAuthLB(d.cr, nil, nil, nil) owner := d.cr.AsOwner() assert.NoError(t, reconcile.VMAuth(ctx, rclient, vmAuth, nil, &owner)) diff --git a/internal/controller/operator/factory/vmdistributed/zone.go b/internal/controller/operator/factory/vmdistributed/zone.go index 46406cee5..c7d4ab774 100644 --- a/internal/controller/operator/factory/vmdistributed/zone.go +++ b/internal/controller/operator/factory/vmdistributed/zone.go @@ -30,10 +30,11 @@ import ( ) type zones struct { - httpClient *http.Client - vmagents []*vmv1beta1.VMAgent - vmclusters []*vmv1beta1.VMCluster - hasChanges []bool + httpClient *http.Client + vmagents []*vmv1beta1.VMAgent + vmclusters []*vmv1beta1.VMCluster + hasChanges []bool + trafficModes []vmv1alpha1.VMDistributedTrafficMode } func (zs *zones) Len() int { @@ -66,6 +67,7 @@ func (zs *zones) Swap(i, j int) { zs.vmagents[i], zs.vmagents[j] = zs.vmagents[j], zs.vmagents[i] zs.vmclusters[i], zs.vmclusters[j] = zs.vmclusters[j], zs.vmclusters[i] zs.hasChanges[i], zs.hasChanges[j] = zs.hasChanges[j], zs.hasChanges[i] + zs.trafficModes[i], zs.trafficModes[j] = zs.trafficModes[j], zs.trafficModes[i] } // getZones builds desired zones @@ -74,9 +76,10 @@ func getZones(ctx context.Context, rclient client.Client, cr *vmv1alpha1.VMDistr httpClient: &http.Client{ Timeout: httpTimeout, }, - vmagents: make([]*vmv1beta1.VMAgent, len(cr.Spec.Zones)), - vmclusters: make([]*vmv1beta1.VMCluster, len(cr.Spec.Zones)), - hasChanges: make([]bool, len(cr.Spec.Zones)), + vmagents: make([]*vmv1beta1.VMAgent, len(cr.Spec.Zones)), + vmclusters: make([]*vmv1beta1.VMCluster, len(cr.Spec.Zones)), + hasChanges: make([]bool, len(cr.Spec.Zones)), + trafficModes: make([]vmv1alpha1.VMDistributedTrafficMode, len(cr.Spec.Zones)), } for i := range cr.Spec.Zones { z := &cr.Spec.Zones[i] @@ -102,8 +105,12 @@ func getZones(ctx context.Context, rclient client.Client, cr *vmv1alpha1.VMDistr prevClusterSpec := vmCluster.Spec vmCluster.Spec = *vmClusterSpec rclient.Scheme().Default(&vmCluster) + if (z.TrafficMode == vmv1alpha1.VMDistributedTrafficModeReadOnly || z.TrafficMode == vmv1alpha1.VMDistributedTrafficModeMaintenance) && vmCluster.Spec.VMInsert != nil { + vmCluster.Spec.VMInsert.ReplicaCount = ptr.To(int32(0)) + } zs.hasChanges[i] = !equality.Semantic.DeepEqual(&vmCluster.Spec, &prevClusterSpec) zs.vmclusters[i] = &vmCluster + zs.trafficModes[i] = z.TrafficMode } for i := range cr.Spec.Zones { @@ -238,7 +245,7 @@ func (zs *zones) updateLB(ctx context.Context, rclient client.Client, cr *vmv1al if !ptr.Deref(cr.Spec.VMAuth.Enabled, true) { return nil } - vmAuth := buildVMAuthLB(cr, zs.vmagents, zs.vmclusters, excludeIds...) + vmAuth := buildVMAuthLB(cr, zs.vmagents, zs.vmclusters, zs.trafficModes, excludeIds...) if vmAuth == nil { return nil } diff --git a/internal/controller/operator/factory/vmdistributed/zone_test.go b/internal/controller/operator/factory/vmdistributed/zone_test.go index 22852d3bf..09339142e 100644 --- a/internal/controller/operator/factory/vmdistributed/zone_test.go +++ b/internal/controller/operator/factory/vmdistributed/zone_test.go @@ -260,9 +260,10 @@ func TestZonesSorting(t *testing.T) { f := func(o opts) { t.Helper() zs := &zones{ - vmclusters: o.clusters, - vmagents: make([]*vmv1beta1.VMAgent, len(o.clusters)), - hasChanges: make([]bool, len(o.clusters)), + vmclusters: o.clusters, + vmagents: make([]*vmv1beta1.VMAgent, len(o.clusters)), + hasChanges: make([]bool, len(o.clusters)), + trafficModes: make([]vmv1alpha1.VMDistributedTrafficMode, len(o.clusters)), } for i := range o.clusters { zs.vmagents[i] = &vmv1beta1.VMAgent{