Skip to content

Commit

Permalink
add WorkerAntiAffinity feature gate and implement validating webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
chrischdi committed Feb 21, 2025
1 parent b92c1c3 commit a0aeaa7
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 1 deletion.
2 changes: 1 addition & 1 deletion config/manager/manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ spec:
- "--diagnostics-address=${CAPI_DIAGNOSTICS_ADDRESS:=:8443}"
- "--insecure-diagnostics=${CAPI_INSECURE_DIAGNOSTICS:=false}"
- --v=4
- "--feature-gates=NodeAntiAffinity=${EXP_NODE_ANTI_AFFINITY:=false},NamespaceScopedZones=${EXP_NAMESPACE_SCOPED_ZONES:=false},PriorityQueue=${EXP_PRIORITY_QUEUE:=false}"
- "--feature-gates=NodeAntiAffinity=${EXP_NODE_ANTI_AFFINITY:=false},NamespaceScopedZones=${EXP_NAMESPACE_SCOPED_ZONES:=false},PriorityQueue=${EXP_PRIORITY_QUEUE:=false},WorkerAntiAffinity=${EXP_WORKER_ANTI_AFFINITY:=false}"
image: controller:latest
imagePullPolicy: IfNotPresent
name: manager
Expand Down
21 changes: 21 additions & 0 deletions config/supervisor/webhook/manifests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@ kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1beta1
clientConfig:
service:
name: webhook-service
namespace: system
path: /validate-vmware-infrastructure-cluster-x-k8s-io-v1beta1-vspherecluster
failurePolicy: Fail
matchPolicy: Equivalent
name: validation.vspherecluster.vmware.infrastructure.cluster.x-k8s.io
rules:
- apiGroups:
- vmware.infrastructure.cluster.x-k8s.io
apiVersions:
- v1beta1
operations:
- CREATE
- UPDATE
resources:
- vsphereclusters
sideEffects: None
- admissionReviewVersions:
- v1beta1
clientConfig:
Expand Down
12 changes: 12 additions & 0 deletions feature/feature.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ const (
//
// alpha: v1.10
PriorityQueue featuregate.Feature = "PriorityQueue"

// WorkerAntiAffinity allows configuring how soft anti-affinity should be done for worker nodes.[]
// If disabled it disallows:
// * mutating `VSphereCluster.spec.placement.workerAntiAffinity.mode`.
// * Setting `MachineDeployment` as value for `VSphereCluster.spec.placement.workerAntiAffinity.mode` on creation.
// Note: the feature requires a version of vm-operator which allows mutation of `VirtualMachineSetResourcePolicy's`.
//
// alpha: v1.13
WorkerAntiAffinity featuregate.Feature = "WorkerAntiAffinity"
)

func init() {
Expand All @@ -57,4 +66,7 @@ var defaultCAPVFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
NodeAntiAffinity: {Default: false, PreRelease: featuregate.Alpha},
NamespaceScopedZones: {Default: false, PreRelease: featuregate.Alpha},
PriorityQueue: {Default: false, PreRelease: featuregate.Alpha},

// Feature gates specific to supervisor mode:
WorkerAntiAffinity: {Default: false, PreRelease: featuregate.Alpha},
}
95 changes: 95 additions & 0 deletions internal/webhooks/vmware/vspherecluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package vmware is the package for webhooks of vmware resources.
package vmware

import (
"context"
"fmt"

apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

vmwarev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
"sigs.k8s.io/cluster-api-provider-vsphere/feature"
"sigs.k8s.io/cluster-api-provider-vsphere/internal/webhooks"
)

// +kubebuilder:webhook:verbs=create;update,path=/validate-vmware-infrastructure-cluster-x-k8s-io-v1beta1-vspherecluster,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=vmware.infrastructure.cluster.x-k8s.io,resources=vsphereclusters,versions=v1beta1,name=validation.vspherecluster.vmware.infrastructure.cluster.x-k8s.io,sideEffects=None,admissionReviewVersions=v1beta1

// VSphereClusterWebhook implements a validation and defaulting webhook for VSphereCluster.
type VSphereClusterWebhook struct{}

var _ webhook.CustomValidator = &VSphereClusterWebhook{}

func (webhook *VSphereClusterWebhook) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(&vmwarev1.VSphereCluster{}).
WithValidator(webhook).
Complete()
}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type.
func (webhook *VSphereClusterWebhook) ValidateCreate(_ context.Context, newRaw runtime.Object) (admission.Warnings, error) {
var allErrs field.ErrorList

newTyped, ok := newRaw.(*vmwarev1.VSphereCluster)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereCluster but got a %T", newRaw))
}

newSpec := newTyped.Spec

if !feature.Gates.Enabled(feature.WorkerAntiAffinity) {
// Cluster mode is not allowed without WorkerAntiAffinity being enabled.
if newSpec.Placement != nil && newSpec.Placement.WorkerAntiAffinity != nil && newSpec.Placement.WorkerAntiAffinity.Mode == vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "placement", "workerAntiAffinity", "mode"), "cannot be set to Cluster with feature-gate WorkerAntiAffinity being disabled"))
}
}

return nil, webhooks.AggregateObjErrors(newTyped.GroupVersionKind().GroupKind(), newTyped.Name, allErrs)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type.
func (webhook *VSphereClusterWebhook) ValidateUpdate(_ context.Context, _ runtime.Object, newRaw runtime.Object) (admission.Warnings, error) {
var allErrs field.ErrorList

newTyped, ok := newRaw.(*vmwarev1.VSphereCluster)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected a VSphereCluster but got a %T", newRaw))
}

newSpec := newTyped.Spec

if !feature.Gates.Enabled(feature.WorkerAntiAffinity) {
// Cluster mode is not allowed without WorkerAntiAffinity being enabled.
if newSpec.Placement != nil && newSpec.Placement.WorkerAntiAffinity != nil && newSpec.Placement.WorkerAntiAffinity.Mode == vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment {
allErrs = append(allErrs, field.Forbidden(field.NewPath("spec", "placement", "workerAntiAffinity", "mode"), "cannot be set to Cluster with feature-gate WorkerAntiAffinity being disabled"))
}
}

return nil, webhooks.AggregateObjErrors(newTyped.GroupVersionKind().GroupKind(), newTyped.Name, allErrs)
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type.
func (webhook *VSphereClusterWebhook) ValidateDelete(_ context.Context, _ runtime.Object) (admission.Warnings, error) {
return nil, nil
}
168 changes: 168 additions & 0 deletions internal/webhooks/vmware/vspherecluster_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package vmware

import (
"context"
"testing"

. "github.com/onsi/gomega"
utilfeature "k8s.io/component-base/featuregate/testing"

vmwarev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
"sigs.k8s.io/cluster-api-provider-vsphere/feature"
)

func TestVSphereCluster_ValidateCreate(t *testing.T) {
tests := []struct {
name string
vsphereCluster *vmwarev1.VSphereCluster
workerAntiAffinity bool
wantErr bool
}{
{
name: "Allow Cluster (WorkerAntiAffinity=false)",
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
workerAntiAffinity: false,
wantErr: false,
},
{
name: "Allow Cluster (WorkerAntiAffinity=true)",
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
workerAntiAffinity: true,
wantErr: false,
},
{
name: "Allow None (WorkerAntiAffinity=false)",
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
workerAntiAffinity: false,
wantErr: false,
},
{
name: "Allow None (WorkerAntiAffinity=true)",
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
workerAntiAffinity: true,
wantErr: false,
},
{
name: "Deny MachineDeployment (WorkerAntiAffinity=false)",
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment),
workerAntiAffinity: false,
wantErr: true,
},
{
name: "Allow MachineDeployment (WorkerAntiAffinity=true)",
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment),
workerAntiAffinity: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.WorkerAntiAffinity, tt.workerAntiAffinity)

webhook := &VSphereClusterWebhook{}
_, err := webhook.ValidateCreate(context.Background(), tt.vsphereCluster)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).NotTo(HaveOccurred())
}
})
}
}
func TestVSphereCluster_ValidateUpdate(t *testing.T) {
tests := []struct {
name string
oldVSphereCluster *vmwarev1.VSphereCluster
vsphereCluster *vmwarev1.VSphereCluster
workerAntiAffinity bool
wantErr bool
}{
{
name: "noop (WorkerAntiAffinity=false)",
oldVSphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
workerAntiAffinity: false,
wantErr: false,
},
{
name: "noop (WorkerAntiAffinity=true)",
oldVSphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
workerAntiAffinity: true,
wantErr: false,
},
{
name: "Allow Cluster to None (WorkerAntiAffinity=false)",
oldVSphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeNone),
workerAntiAffinity: false,
wantErr: false,
},
{
name: "Allow Cluster to None (WorkerAntiAffinity=true)",
oldVSphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeNone),
workerAntiAffinity: true,
wantErr: false,
},
{
name: "Disallow Cluster to MachineDeployment (WorkerAntiAffinity=false)",
oldVSphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment),
workerAntiAffinity: false,
wantErr: true,
},
{
name: "Allow Cluster to MachineDeployment (WorkerAntiAffinity=true)",
oldVSphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster),
vsphereCluster: createVSphereCluster(vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment),
workerAntiAffinity: true,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.WorkerAntiAffinity, tt.workerAntiAffinity)

webhook := &VSphereClusterWebhook{}
_, err := webhook.ValidateUpdate(context.Background(), tt.oldVSphereCluster, tt.vsphereCluster)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
} else {
g.Expect(err).NotTo(HaveOccurred())
}
})
}
}

func createVSphereCluster(mode vmwarev1.VSphereClusterWorkerAntiAffinityMode) *vmwarev1.VSphereCluster {
vSphereCluster := &vmwarev1.VSphereCluster{}
if mode != "" {
vSphereCluster.Spec.Placement = &vmwarev1.VSphereClusterPlacement{
WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: mode,
},
}
}
return vSphereCluster
}
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,9 @@ func setupVAPIControllers(ctx context.Context, controllerCtx *capvcontext.Contro
}

func setupSupervisorControllers(ctx context.Context, controllerCtx *capvcontext.ControllerManagerContext, mgr ctrlmgr.Manager, clusterCache clustercache.ClusterCache) error {
if err := (&vmwarewebhooks.VSphereClusterWebhook{}).SetupWebhookWithManager(mgr); err != nil {
return err
}
if err := (&vmwarewebhooks.VSphereMachineTemplateWebhook{}).SetupWebhookWithManager(mgr); err != nil {
return err
}
Expand Down

0 comments on commit a0aeaa7

Please sign in to comment.