Skip to content

Commit adbada8

Browse files
committed
Add global override for vmcluster
1 parent a09ac7d commit adbada8

File tree

6 files changed

+191
-5
lines changed

6 files changed

+191
-5
lines changed

api/operator/v1alpha1/vmdistributedcluster_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ type VMDistributedClusterSpec struct {
6363
// +k8s:openapi-gen=true
6464
// ZoneSpec is a list of VMCluster instances to update.
6565
type ZoneSpec struct {
66+
// GlobalOverrideSpec specifies an override to all VMClusters.
67+
// These overrides are applied to the referenced object if `Ref` is specified.
68+
// +kubebuilder:validation:Type=object
69+
// +kubebuilder:validation:XPreserveUnknownFields
70+
// +optional
71+
GlobalOverrideSpec *apiextensionsv1.JSON `json:"globalOverrideSpec,omitempty"`
72+
6673
// Each VMClusterRefOrSpec is either defining a new inline VMCluster or referencing an existing one.
6774
VMClusters []VMClusterRefOrSpec `json:"vmclusters,omitempty"`
6875
}
@@ -75,6 +82,7 @@ type VMClusterRefOrSpec struct {
7582
// If Ref is specified, Name and Spec are ignored.
7683
// +optional
7784
Ref *corev1.LocalObjectReference `json:"ref,omitempty"`
85+
7886
// OverrideSpec specifies an override to the VMClusterSpec of the referenced object.
7987
// This override is applied to the referenced object if `Ref` is specified.
8088
// This field is ignored if `Spec` is specified.

api/operator/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/overlay/crd.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23460,6 +23460,9 @@ spec:
2346023460
type: string
2346123461
zones:
2346223462
properties:
23463+
globalOverrideSpec:
23464+
type: object
23465+
x-kubernetes-preserve-unknown-fields: true
2346323466
vmclusters:
2346423467
items:
2346523468
properties:

internal/controller/operator/factory/vmdistributedcluster/vmcluster_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,70 @@ func TestValidateVMClusterRefOrSpec_Matrix(t *testing.T) {
350350
assert.Error(t, validateVMClusterRefOrSpec(6, bad))
351351
}
352352

353+
func TestApplyGlobalOverrideSpec(t *testing.T) {
354+
base := vmv1beta1.VMClusterSpec{
355+
ClusterVersion: "v1.0.0",
356+
ServiceAccountName: "base",
357+
RetentionPeriod: "30d",
358+
}
359+
360+
// Test with nil GlobalOverrideSpec
361+
globalOverride := (*apiextensionsv1.JSON)(nil)
362+
merged, modified, err := ApplyOverrideSpec(base, globalOverride)
363+
assert.NoError(t, err)
364+
assert.False(t, modified)
365+
assert.Equal(t, base, merged)
366+
367+
// Test with empty GlobalOverrideSpec
368+
emptyGlobal := &apiextensionsv1.JSON{Raw: []byte("{}")}
369+
merged2, modified2, err := ApplyOverrideSpec(base, emptyGlobal)
370+
assert.NoError(t, err)
371+
assert.False(t, modified2)
372+
assert.Equal(t, base, merged2)
373+
374+
// Test with GlobalOverrideSpec that modifies top-level fields
375+
globalTopLevel := &apiextensionsv1.JSON{Raw: []byte(`{"clusterVersion": "v2.0.0", "serviceAccountName": "global-sa"}`)}
376+
merged3, modified3, err := ApplyOverrideSpec(base, globalTopLevel)
377+
assert.NoError(t, err)
378+
assert.True(t, modified3)
379+
assert.Equal(t, "v2.0.0", merged3.ClusterVersion)
380+
assert.Equal(t, "global-sa", merged3.ServiceAccountName)
381+
assert.Equal(t, "30d", merged3.RetentionPeriod) // Unchanged field
382+
383+
// Test with GlobalOverrideSpec that sets a field to null
384+
globalNullify := &apiextensionsv1.JSON{Raw: []byte(`{"serviceAccountName": null}`)}
385+
merged5, modified5, err := ApplyOverrideSpec(base, globalNullify)
386+
assert.NoError(t, err)
387+
assert.True(t, modified5)
388+
assert.Equal(t, "", merged5.ServiceAccountName) // Should be empty string (null in JSON becomes empty string after unmarshal)
389+
}
390+
391+
func TestApplyGlobalAndClusterSpecificOverrideSpecs(t *testing.T) {
392+
base := vmv1beta1.VMClusterSpec{
393+
ClusterVersion: "v1.0.0",
394+
ServiceAccountName: "base",
395+
RetentionPeriod: "30d",
396+
}
397+
398+
// Apply GlobalOverrideSpec first
399+
globalOverride := &apiextensionsv1.JSON{Raw: []byte(`{"clusterVersion": "v2.0.0", "serviceAccountName": "global-sa"}`)}
400+
merged1, modified1, err := ApplyOverrideSpec(base, globalOverride)
401+
assert.NoError(t, err)
402+
assert.True(t, modified1)
403+
assert.Equal(t, "v2.0.0", merged1.ClusterVersion)
404+
assert.Equal(t, "global-sa", merged1.ServiceAccountName)
405+
assert.Equal(t, "30d", merged1.RetentionPeriod)
406+
407+
// Then apply cluster-specific override
408+
clusterOverride := &apiextensionsv1.JSON{Raw: []byte(`{"retentionPeriod": "10d", "clusterVersion": "v3.0.0"}`)}
409+
merged2, modified2, err := ApplyOverrideSpec(merged1, clusterOverride)
410+
assert.NoError(t, err)
411+
assert.True(t, modified2)
412+
assert.Equal(t, "v3.0.0", merged2.ClusterVersion) // Cluster-specific override should take precedence
413+
assert.Equal(t, "global-sa", merged2.ServiceAccountName) // From global override, unchanged by cluster override
414+
assert.Equal(t, "10d", merged2.RetentionPeriod) // From cluster override
415+
}
416+
353417
// helpers
354418
func ptrTo[T any](v T) *T { return &v }
355419

internal/controller/operator/factory/vmdistributedcluster/vmdistributedcluster.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,25 @@ func CreateOrUpdate(ctx context.Context, cr *vmv1alpha1.VMDistributedCluster, rc
116116
}
117117

118118
// Update the VMCluster when overrideSpec needs to be applied or ownerref set
119-
var mergedSpec vmv1beta1.VMClusterSpec
120-
var modifiedSpec bool
119+
mergedSpec := vmClusterObj.Spec
120+
modifiedSpec := false
121121

122-
if zoneRefOrSpec.Ref != nil {
123-
mergedSpec, modifiedSpec, err = ApplyOverrideSpec(vmClusterObj.Spec, zoneRefOrSpec.OverrideSpec)
122+
// Apply GlobalOverrideSpec if it is set
123+
if cr.Spec.Zones.GlobalOverrideSpec != nil {
124+
mergedSpec, modifiedSpec, err = ApplyOverrideSpec(vmClusterObj.Spec, cr.Spec.Zones.GlobalOverrideSpec)
125+
if err != nil {
126+
return fmt.Errorf("failed to apply global override spec for vmcluster %s at index %d: %w", vmClusterObj.Name, i, err)
127+
}
128+
}
129+
// Apply cluster-specific override if it exist
130+
if zoneRefOrSpec.Ref != nil && zoneRefOrSpec.OverrideSpec != nil {
131+
mergedSpec, modifiedSpec, err = ApplyOverrideSpec(mergedSpec, zoneRefOrSpec.OverrideSpec)
124132
if err != nil {
125133
return fmt.Errorf("failed to apply override spec for vmcluster %s at index %d: %w", vmClusterObj.Name, i, err)
126134
}
127135
}
128136
if zoneRefOrSpec.Spec != nil {
129-
mergedSpec, modifiedSpec, err = mergeVMClusterSpecs(vmClusterObj.Spec, *zoneRefOrSpec.Spec)
137+
mergedSpec, modifiedSpec, err = mergeVMClusterSpecs(mergedSpec, *zoneRefOrSpec.Spec)
130138
if err != nil {
131139
return fmt.Errorf("failed to merge spec for vmcluster %s at index %d: %w", vmClusterObj.Name, i, err)
132140
}

test/e2e/vmdistributedcluster_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,4 +1003,102 @@ var _ = Describe("e2e vmdistributedcluster", Label("vm", "vmdistributedcluster")
10031003
}
10041004
})
10051005
})
1006+
1007+
Describe("GlobalOverrideSpec", func() {
1008+
It("should apply global overrides before cluster-specific overrides", func() {
1009+
By("creating initial VMClusters")
1010+
vmCluster1 := &vmv1beta1.VMCluster{
1011+
ObjectMeta: metav1.ObjectMeta{
1012+
Namespace: namespace,
1013+
Name: "vmcluster-global-1",
1014+
},
1015+
Spec: vmv1beta1.VMClusterSpec{
1016+
ClusterVersion: "v1.126.0-cluster",
1017+
RetentionPeriod: "30d",
1018+
VMStorage: &vmv1beta1.VMStorage{
1019+
CommonApplicationDeploymentParams: vmv1beta1.CommonApplicationDeploymentParams{
1020+
ReplicaCount: ptr.To[int32](1),
1021+
},
1022+
},
1023+
},
1024+
}
1025+
createVMClusterAndEnsureOperational(ctx, k8sClient, vmCluster1, namespace)
1026+
1027+
vmCluster2 := &vmv1beta1.VMCluster{
1028+
ObjectMeta: metav1.ObjectMeta{
1029+
Namespace: namespace,
1030+
Name: "vmcluster-global-2",
1031+
},
1032+
Spec: vmv1beta1.VMClusterSpec{
1033+
ClusterVersion: "v1.126.0-cluster",
1034+
RetentionPeriod: "30d",
1035+
VMStorage: &vmv1beta1.VMStorage{
1036+
CommonApplicationDeploymentParams: vmv1beta1.CommonApplicationDeploymentParams{
1037+
ReplicaCount: ptr.To[int32](1),
1038+
},
1039+
},
1040+
},
1041+
}
1042+
createVMClusterAndEnsureOperational(ctx, k8sClient, vmCluster2, namespace)
1043+
1044+
By("creating a VMDistributedCluster with GlobalOverrideSpec and cluster-specific overrides")
1045+
namespacedName.Name = "global-override-cluster"
1046+
cr := &vmv1alpha1.VMDistributedCluster{
1047+
ObjectMeta: metav1.ObjectMeta{
1048+
Namespace: namespace,
1049+
Name: namespacedName.Name,
1050+
},
1051+
Spec: vmv1alpha1.VMDistributedClusterSpec{
1052+
VMAgentFlushDeadline: &metav1.Duration{Duration: 1 * time.Second},
1053+
ZoneUpdatePause: &metav1.Duration{Duration: 1 * time.Second},
1054+
VMAgent: vmv1alpha1.VMAgentNameAndSpec{Name: existingVMAgentName},
1055+
VMAuth: vmv1alpha1.VMAuthNameAndSpec{Name: existingVMAuthName},
1056+
Zones: vmv1alpha1.ZoneSpec{
1057+
GlobalOverrideSpec: &apiextensionsv1.JSON{
1058+
Raw: []byte(`{"clusterVersion": "v1.127.0-cluster", "retentionPeriod": "60d"}`),
1059+
},
1060+
VMClusters: []vmv1alpha1.VMClusterRefOrSpec{
1061+
{
1062+
Ref: &corev1.LocalObjectReference{Name: vmCluster1.Name},
1063+
OverrideSpec: &apiextensionsv1.JSON{
1064+
Raw: []byte(`{"vmStorage": {"replicaCount": 2}}`),
1065+
},
1066+
},
1067+
{
1068+
Ref: &corev1.LocalObjectReference{Name: vmCluster2.Name},
1069+
OverrideSpec: &apiextensionsv1.JSON{
1070+
Raw: []byte(`{"vmStorage": {"replicaCount": 3}}`),
1071+
},
1072+
},
1073+
},
1074+
},
1075+
},
1076+
}
1077+
DeferCleanup(func() {
1078+
Expect(finalize.SafeDelete(ctx, k8sClient, cr)).To(Succeed())
1079+
})
1080+
Expect(k8sClient.Create(ctx, cr)).To(Succeed())
1081+
1082+
By("waiting for VMDistributedCluster to become operational")
1083+
Eventually(func() error {
1084+
return expectObjectStatusOperational(ctx, k8sClient, &vmv1alpha1.VMDistributedCluster{}, namespacedName)
1085+
}, eventualVMDistributedClusterExpandingTimeout).WithContext(ctx).Should(Succeed())
1086+
verifyOwnerReferences(ctx, cr, []vmv1beta1.VMCluster{*vmCluster1, *vmCluster2}, namespace)
1087+
1088+
By("verifying that both VMClusters have the global override applied")
1089+
var updatedCluster1, updatedCluster2 vmv1beta1.VMCluster
1090+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: vmCluster1.Name, Namespace: namespace}, &updatedCluster1)).To(Succeed())
1091+
Expect(k8sClient.Get(ctx, types.NamespacedName{Name: vmCluster2.Name, Namespace: namespace}, &updatedCluster2)).To(Succeed())
1092+
1093+
// Global overrides should be applied
1094+
Expect(updatedCluster1.Spec.ClusterVersion).To(Equal("v1.127.0-cluster"))
1095+
Expect(updatedCluster1.Spec.RetentionPeriod).To(Equal("60d"))
1096+
Expect(updatedCluster2.Spec.ClusterVersion).To(Equal("v1.127.0-cluster"))
1097+
Expect(updatedCluster2.Spec.RetentionPeriod).To(Equal("60d"))
1098+
1099+
// Cluster-specific overrides should be applied after global overrides
1100+
Expect(*updatedCluster1.Spec.VMStorage.ReplicaCount).To(Equal(int32(2)))
1101+
Expect(*updatedCluster2.Spec.VMStorage.ReplicaCount).To(Equal(int32(3)))
1102+
})
1103+
})
10061104
})

0 commit comments

Comments
 (0)