Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🐛 [WIP] supervisor: create ClusterModule per MachineDeployment and re-reconcile VirtualMachineResourceSetPolicy to update VirtualMachineSetResourcePolicy #3287

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions apis/vmware/v1beta1/vspherecluster_types.go
Original file line number Diff line number Diff line change
@@ -38,8 +38,43 @@ const (
type VSphereClusterSpec struct {
// +optional
ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
// placement allows to configure the placement of machines of a VSphereCluster.
// +optional
Placement *VSphereClusterPlacement `json:"placement,omitempty"`
}

// VSphereClusterPlacement defines a placement strategy for machines of a VSphereCluster.
// +kubebuilder:validation:MinProperties=1
type VSphereClusterPlacement struct {
// workerAntiAffinity configures soft anti-affinity for workers.
// +optional
WorkerAntiAffinity *VSphereClusterWorkerAntiAffinity `json:"workerAntiAffinity,omitempty"`
}

// VSphereClusterWorkerAntiAffinity defines the anti-affinity configuration for workers.
// +kubebuilder:validation:MinProperties=1
type VSphereClusterWorkerAntiAffinity struct {
// mode allows to set the grouping of (soft) anti-affinity for worker nodes.
// Defaults to `Cluster`.
// +kubebuilder:validation:Enum=Cluster;None;MachineDeployment
// +optional
Mode VSphereClusterWorkerAntiAffinityMode `json:"mode,omitempty"`
}

// VSphereClusterWorkerAntiAffinityMode describes the soft anti-affinity mode used across a for distributing virtual machines.
type VSphereClusterWorkerAntiAffinityMode string

const (
// VSphereClusterWorkerAntiAffinityModeCluster means to use all workers as a single group for soft anti-affinity.
VSphereClusterWorkerAntiAffinityModeCluster VSphereClusterWorkerAntiAffinityMode = "Cluster"

// VSphereClusterWorkerAntiAffinityModeNone means to not configure any soft anti-affinity for workers.
VSphereClusterWorkerAntiAffinityModeNone VSphereClusterWorkerAntiAffinityMode = "None"

// VSphereClusterWorkerAntiAffinityModeMachineDeployment means to configure soft anti-affinity for all workers per MachineDeployment.
VSphereClusterWorkerAntiAffinityModeMachineDeployment VSphereClusterWorkerAntiAffinityMode = "MachineDeployment"
)

// VSphereClusterStatus defines the observed state of VSphereClusterSpec.
type VSphereClusterStatus struct {
// Ready indicates the infrastructure required to deploy this cluster is
48 changes: 44 additions & 4 deletions apis/vmware/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion config/manager/manager.yaml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -55,6 +55,27 @@ spec:
- host
- port
type: object
placement:
description: placement allows to configure the placement of machines
of a VSphereCluster.
minProperties: 1
properties:
workerAntiAffinity:
description: workerAntiAffinity configures soft anti-affinity
for workers.
minProperties: 1
properties:
mode:
description: |-
mode allows to set the grouping of (soft) anti-affinity for worker nodes.
Defaults to `Cluster`.
enum:
- Cluster
- None
- MachineDeployment
type: string
type: object
type: object
type: object
status:
description: VSphereClusterStatus defines the observed state of VSphereClusterSpec.
Original file line number Diff line number Diff line change
@@ -65,6 +65,27 @@ spec:
- host
- port
type: object
placement:
description: placement allows to configure the placement of
machines of a VSphereCluster.
minProperties: 1
properties:
workerAntiAffinity:
description: workerAntiAffinity configures soft anti-affinity
for workers.
minProperties: 1
properties:
mode:
description: |-
mode allows to set the grouping of (soft) anti-affinity for worker nodes.
Defaults to `Cluster`.
enum:
- Cluster
- None
- MachineDeployment
type: string
type: object
type: object
type: object
required:
- spec
21 changes: 21 additions & 0 deletions config/supervisor/webhook/manifests.yaml
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion controllers/vmware/test/controllers_test.go
Original file line number Diff line number Diff line change
@@ -478,7 +478,7 @@ var _ = Describe("Reconciliation tests", func() {
Eventually(func() error {
return k8sClient.Get(ctx, rpKey, resourcePolicy)
}, time.Second*30).Should(Succeed())
Expect(len(resourcePolicy.Spec.ClusterModuleGroups)).To(BeEquivalentTo(2))
Expect(len(resourcePolicy.Spec.ClusterModuleGroups)).To(BeEquivalentTo(1))

By("Create the CAPI Machine and wait for it to exist")
machineKey, machine := deployCAPIMachine(ns.Name, cluster, k8sClient)
32 changes: 31 additions & 1 deletion controllers/vmware/vspherecluster_reconciler.go
Original file line number Diff line number Diff line change
@@ -69,6 +69,8 @@ type ClusterReconciler struct {
// +kubebuilder:rbac:groups=netoperator.vmware.com,resources=networks,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;update;create;delete
// +kubebuilder:rbac:groups="",resources=persistentvolumeclaims/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machinedeployments,verbs=get;list;watch
// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=machines,verbs=get;list;watch

func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) {
log := ctrl.LoggerFrom(ctx)
@@ -172,7 +174,7 @@ func (r *ClusterReconciler) reconcileNormal(ctx context.Context, clusterCtx *vmw
// Reconcile ResourcePolicy before we create the machines. If the ResourcePolicy is not reconciled before we create the Node VMs,
// it will be handled by vm operator by relocating the VMs to the ResourcePool and Folder specified by the ResourcePolicy.
// Reconciling the ResourcePolicy early potentially saves us the extra relocate operation.
resourcePolicyName, err := r.ResourcePolicyService.ReconcileResourcePolicy(ctx, clusterCtx)
resourcePolicyName, err := r.ResourcePolicyService.ReconcileResourcePolicy(ctx, clusterCtx.Cluster, clusterCtx.VSphereCluster)
if err != nil {
conditions.MarkFalse(clusterCtx.VSphereCluster, vmwarev1.ResourcePolicyReadyCondition, vmwarev1.ResourcePolicyCreationFailedReason, clusterv1.ConditionSeverityWarning, err.Error())
return errors.Wrapf(err,
@@ -370,6 +372,34 @@ func (r *ClusterReconciler) VSphereMachineToCluster(ctx context.Context, o clien
}}
}

// MachineDeploymentToCluster adds reconcile requests for a Cluster when one of its machineDeployments has an event.
func (r *ClusterReconciler) MachineDeploymentToCluster(ctx context.Context, o client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx)

machineDeployment, ok := o.(*clusterv1.MachineDeployment)
if !ok {
log.Error(nil, fmt.Sprintf("Expected a MachineDeployment but got a %T", o))
return nil
}
log = log.WithValues("MachineDeployment", klog.KObj(machineDeployment))
ctx = ctrl.LoggerInto(ctx, log)

vsphereCluster, err := util.GetVMwareVSphereClusterFromMachineDeployment(ctx, r.Client, machineDeployment)
if err != nil {
log.V(4).Error(err, "Failed to get VSphereCluster from MachineDeployment")
return nil
}

// Can add further filters on Cluster state so that we don't keep reconciling Cluster
log.V(6).Info("Triggering VSphereCluster reconcile from MachineDeployment")
return []ctrl.Request{{
NamespacedName: types.NamespacedName{
Namespace: vsphereCluster.Namespace,
Name: vsphereCluster.Name,
},
}}
}

// ZoneToVSphereClusters adds reconcile requests for VSphereClusters when Zone has an event.
func (r *ClusterReconciler) ZoneToVSphereClusters(ctx context.Context, o client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx)
4 changes: 4 additions & 0 deletions controllers/vspherecluster_controller.go
Original file line number Diff line number Diff line change
@@ -83,6 +83,10 @@ func AddClusterControllerToManager(ctx context.Context, controllerManagerCtx *ca
&vmwarev1.VSphereMachine{},
handler.EnqueueRequestsFromMapFunc(reconciler.VSphereMachineToCluster),
).
Watches(
&clusterv1.MachineDeployment{},
handler.EnqueueRequestsFromMapFunc(reconciler.MachineDeploymentToCluster),
).
WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(mgr.GetScheme(), predicateLog, controllerManagerCtx.WatchFilterValue))

// Conditionally add a Watch for topologyv1.Zone when the feature gate is enabled
12 changes: 12 additions & 0 deletions feature/feature.go
Original file line number Diff line number Diff line change
@@ -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() {
@@ -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
@@ -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
}
3 changes: 2 additions & 1 deletion pkg/services/interfaces.go
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"

infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
vmwarev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
capvcontext "sigs.k8s.io/cluster-api-provider-vsphere/pkg/context"
"sigs.k8s.io/cluster-api-provider-vsphere/pkg/context/vmware"
)
@@ -63,7 +64,7 @@ type ControlPlaneEndpointService interface {
type ResourcePolicyService interface {
// ReconcileResourcePolicy ensures that a VirtualMachineSetResourcePolicy exists for the cluster
// Returns the name of a policy if it exists, otherwise returns an error
ReconcileResourcePolicy(ctx context.Context, clusterCtx *vmware.ClusterContext) (string, error)
ReconcileResourcePolicy(ctx context.Context, cluster *clusterv1.Cluster, vSphereCluster *vmwarev1.VSphereCluster) (string, error)
}

// NetworkProvider provision network resources and configures VM based on network type.
2 changes: 2 additions & 0 deletions pkg/services/vmoperator/constants.go
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@ const (

// ControlPlaneVMClusterModuleGroupName is the name used for the control plane Cluster Module.
ControlPlaneVMClusterModuleGroupName = "control-plane-group"
// ClusterWorkerVMClusterModuleGroupName is the name used for the worker Cluster Module when using mode Cluster.
ClusterWorkerVMClusterModuleGroupName = "workers-group"
// ClusterModuleNameAnnotationKey is key for the Cluster Module annotation.
ClusterModuleNameAnnotationKey = "vsphere-cluster-module-group"
// ProviderTagsAnnotationKey is the key used for the provider tags annotation.
207 changes: 158 additions & 49 deletions pkg/services/vmoperator/resource_policy.go
Original file line number Diff line number Diff line change
@@ -18,15 +18,22 @@ package vmoperator

import (
"context"
"fmt"
"sort"

"github.com/pkg/errors"
vmoprv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/klog/v2"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/util/patch"
"sigs.k8s.io/controller-runtime/pkg/client"
ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

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

// RPService represents the ability to reconcile a VirtualMachineSetResourcePolicy via vmoperator.
@@ -36,73 +43,175 @@ type RPService struct {

// ReconcileResourcePolicy ensures that a VirtualMachineSetResourcePolicy exists for the cluster
// Returns the name of a policy if it exists, otherwise returns an error.
func (s *RPService) ReconcileResourcePolicy(ctx context.Context, clusterCtx *vmware.ClusterContext) (string, error) {
resourcePolicy, err := s.getVirtualMachineSetResourcePolicy(ctx, clusterCtx)
func (s *RPService) ReconcileResourcePolicy(ctx context.Context, cluster *clusterv1.Cluster, vSphereCluster *vmwarev1.VSphereCluster) (string, error) {
clusterModuleGroups, err := getTargetClusterModuleGroups(ctx, s.Client, cluster, vSphereCluster)
if err != nil {
return "", err
}

resourcePolicyName := cluster.Name
resourcePolicy := &vmoprv1.VirtualMachineSetResourcePolicy{}

if err := s.Client.Get(ctx, client.ObjectKey{Namespace: cluster.Namespace, Name: resourcePolicyName}, resourcePolicy); err != nil {
if !apierrors.IsNotFound(err) {
return "", errors.Errorf("unexpected error in getting the Resource policy: %+v", err)
return "", errors.Wrap(err, "failed to get existing VirtualMachineSetResourcePolicy")
}
resourcePolicy, err = s.createVirtualMachineSetResourcePolicy(ctx, clusterCtx)
if err != nil {
return "", errors.Errorf("failed to create Resource Policy: %+v", err)

resourcePolicy = &vmoprv1.VirtualMachineSetResourcePolicy{
ObjectMeta: metav1.ObjectMeta{
Namespace: cluster.Namespace,
Name: resourcePolicyName,
},
}

if err := s.mutateResourcePolicy(resourcePolicy, clusterModuleGroups, cluster, vSphereCluster, true); err != nil {
return "", errors.Wrap(err, "failed to mutate VirtualMachineSetResourcePolicy")
}

if err := s.Client.Create(ctx, resourcePolicy); err != nil {
return "", errors.Wrap(err, "failed to create VirtualMachineSetResourcePolicy")
}

return resourcePolicyName, nil
}

// Ensure .spec.clusterModuleGroups is up to date.
helper, err := patch.NewHelper(resourcePolicy, s.Client)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I prefer this approach (using the patch helper), but let's consider also using createOrUpdate for consistency with the rest of the codebase

if err != nil {
return "", err
}

if err := s.mutateResourcePolicy(resourcePolicy, clusterModuleGroups, cluster, vSphereCluster, false); err != nil {
return "", errors.Wrap(err, "failed to mutate VirtualMachineSetResourcePolicy")
}

resourcePolicy.Spec.ClusterModuleGroups = clusterModuleGroups
if err := helper.Patch(ctx, resourcePolicy); err != nil {
return "", err
}

return resourcePolicy.Name, nil
return resourcePolicyName, nil
}

func (s *RPService) newVirtualMachineSetResourcePolicy(clusterCtx *vmware.ClusterContext) *vmoprv1.VirtualMachineSetResourcePolicy {
return &vmoprv1.VirtualMachineSetResourcePolicy{
ObjectMeta: metav1.ObjectMeta{
Namespace: clusterCtx.Cluster.Namespace,
Name: clusterCtx.Cluster.Name,
},
func (s *RPService) mutateResourcePolicy(resourcePolicy *vmoprv1.VirtualMachineSetResourcePolicy, clusterModuleGroups []string, cluster *clusterv1.Cluster, vSphereCluster *vmwarev1.VSphereCluster, isCreate bool) error {
// Always ensure the owner reference
if err := ctrlutil.SetOwnerReference(vSphereCluster, resourcePolicy, s.Client.Scheme()); err != nil {
return errors.Wrapf(err, "failed to set owner reference for virtualMachineSetResourcePolicy %s for cluster %s", klog.KObj(resourcePolicy), klog.KObj(vSphereCluster))
}

// Always ensure the clusterModuleGroups are up-to-date.
resourcePolicy.Spec.ClusterModuleGroups = clusterModuleGroups

// On create: Also set resourcePool and folder
if isCreate {
resourcePolicy.Spec.Folder = cluster.Name
resourcePolicy.Spec.ResourcePool = vmoprv1.ResourcePoolSpec{
Name: cluster.Name,
}
}

return nil
}

func (s *RPService) getVirtualMachineSetResourcePolicy(ctx context.Context, clusterCtx *vmware.ClusterContext) (*vmoprv1.VirtualMachineSetResourcePolicy, error) {
func getVirtualMachineSetResourcePolicy(ctx context.Context, ctrlClient client.Client, cluster *clusterv1.Cluster) (*vmoprv1.VirtualMachineSetResourcePolicy, error) {
vmResourcePolicy := &vmoprv1.VirtualMachineSetResourcePolicy{}
vmResourcePolicyName := client.ObjectKey{
Namespace: clusterCtx.Cluster.Namespace,
Name: clusterCtx.Cluster.Name,
Namespace: cluster.Namespace,
Name: cluster.Name,
}
err := s.Client.Get(ctx, vmResourcePolicyName, vmResourcePolicy)
return vmResourcePolicy, err
if err := ctrlClient.Get(ctx, vmResourcePolicyName, vmResourcePolicy); err != nil {
return nil, err
}

return vmResourcePolicy, nil
}

func (s *RPService) createVirtualMachineSetResourcePolicy(ctx context.Context, clusterCtx *vmware.ClusterContext) (*vmoprv1.VirtualMachineSetResourcePolicy, error) {
vmResourcePolicy := s.newVirtualMachineSetResourcePolicy(clusterCtx)
func getFallbackWorkerClusterModuleGroupName(clusterName string) string {
return fmt.Sprintf("%s-workers-0", clusterName)
}

_, err := ctrlutil.CreateOrPatch(ctx, s.Client, vmResourcePolicy, func() error {
vmResourcePolicy.Spec = vmoprv1.VirtualMachineSetResourcePolicySpec{
ResourcePool: vmoprv1.ResourcePoolSpec{
Name: clusterCtx.Cluster.Name,
},
Folder: clusterCtx.Cluster.Name,
ClusterModuleGroups: []string{
ControlPlaneVMClusterModuleGroupName,
getMachineDeploymentNameForCluster(clusterCtx.Cluster),
},
}
// Ensure that the VirtualMachineSetResourcePolicy is owned by the VSphereCluster
if err := ctrlutil.SetOwnerReference(
clusterCtx.VSphereCluster,
vmResourcePolicy,
s.Client.Scheme(),
); err != nil {
return errors.Wrapf(
err,
"error setting %s/%s as owner of %s/%s",
clusterCtx.VSphereCluster.Namespace,
clusterCtx.VSphereCluster.Name,
vmResourcePolicy.Namespace,
vmResourcePolicy.Name,
)
func getWorkerAntiAffinityMode(vSphereCluster *vmwarev1.VSphereCluster) vmwarev1.VSphereClusterWorkerAntiAffinityMode {
if vSphereCluster.Spec.Placement == nil || vSphereCluster.Spec.Placement.WorkerAntiAffinity == nil {
return vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster
}

return vSphereCluster.Spec.Placement.WorkerAntiAffinity.Mode
}

func getTargetClusterModuleGroups(ctx context.Context, ctrlClient client.Client, cluster *clusterv1.Cluster, vSphereCluster *vmwarev1.VSphereCluster) ([]string, error) {
if !feature.Gates.Enabled(feature.WorkerAntiAffinity) {
// Fallback to old behaviour
return []string{
ControlPlaneVMClusterModuleGroupName,
getFallbackWorkerClusterModuleGroupName(cluster.Name),
}, nil
}
// Always add a cluster module for control plane machines.
modules := []string{
ControlPlaneVMClusterModuleGroupName,
}

switch mode := getWorkerAntiAffinityMode(vSphereCluster); mode {
case vmwarev1.VSphereClusterWorkerAntiAffinityModeNone:
// Only configure a cluster module for control-plane nodes
case vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster:
// Add an additional cluster module for workers when using Cluster mode.
modules = append(modules, ClusterWorkerVMClusterModuleGroupName)
case vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment:
// Add an additional cluster module for each MachineDeployment workers when using MachineDeployment mode.
machineDeploymentNames, err := getMachineDeploymentNamesForCluster(ctx, ctrlClient, cluster)
if err != nil {
return nil, err
}
return nil
})

modules = append(modules, machineDeploymentNames...)
default:
return nil, errors.Errorf("unknown mode %q configured for WorkerAntiAffinity", mode)
}

// Add cluster modules from existing VirtualMachines and deduplicate with the target ones.
existingModules, err := getVirtualMachineClusterModulesForCluster(ctx, ctrlClient, cluster)
if err != nil {
return nil, err
}
return vmResourcePolicy, nil
modules = existingModules.Insert(modules...).UnsortedList()

// Sort elements to have deterministic output.
sort.Strings(modules)

return modules, nil
}

func getVirtualMachineClusterModulesForCluster(ctx context.Context, ctrlClient client.Client, cluster *clusterv1.Cluster) (sets.Set[string], error) {
labels := map[string]string{clusterv1.ClusterNameLabel: cluster.GetName()}
virtualMachineList := &vmoprv1.VirtualMachineList{}
if err := ctrlClient.List(
ctx, virtualMachineList,
client.InNamespace(cluster.GetNamespace()),
client.MatchingLabels(labels)); err != nil {
return nil, errors.Wrapf(err, "failed to list MachineDeployment objects")
}

clusterModules := sets.Set[string]{}
for _, virtualMachine := range virtualMachineList.Items {
if clusterModule, ok := virtualMachine.Annotations[ClusterModuleNameAnnotationKey]; ok {
clusterModules = clusterModules.Insert(clusterModule)
}
}
return clusterModules, nil
}

func checkClusterModuleGroup(ctx context.Context, ctrlClient client.Client, cluster *clusterv1.Cluster, clusterModuleGroupName string) error {
resourcePolicy, err := getVirtualMachineSetResourcePolicy(ctx, ctrlClient, cluster)
if err != nil {
return err
}

for _, cm := range resourcePolicy.Status.ClusterModules {
if cm.GroupName == clusterModuleGroupName {
return nil
}
}

return errors.Errorf("VirtualMachineSetResourcePolicy's .status.clusterModules does not yet contain group %q", clusterModuleGroupName)
}
153 changes: 131 additions & 22 deletions pkg/services/vmoperator/resource_policy_test.go
Original file line number Diff line number Diff line change
@@ -18,35 +18,144 @@ package vmoperator

import (
"context"
"fmt"
"testing"

. "github.com/onsi/gomega"
capi_util "sigs.k8s.io/cluster-api/util"
vmoprv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilfeature "k8s.io/component-base/featuregate/testing"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

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

func TestRPService(t *testing.T) {
clusterName := "test-cluster"
vsphereClusterName := fmt.Sprintf("%s-%s", clusterName, capi_util.RandomString((6)))
cluster := util.CreateCluster(clusterName)
vsphereCluster := util.CreateVSphereCluster(vsphereClusterName)
clusterCtx, controllerCtx := util.CreateClusterContext(cluster, vsphereCluster)
func TestRPService_ReconcileResourcePolicy(t *testing.T) {
scheme := runtime.NewScheme()
_ = vmwarev1.AddToScheme(scheme)
_ = clusterv1.AddToScheme(scheme)
_ = vmoprv1.AddToScheme(scheme)
ctx := context.Background()
rpService := RPService{
Client: controllerCtx.Client,

tests := []struct {
name string
cluster *clusterv1.Cluster
vSphereCluster *vmwarev1.VSphereCluster ``
additionalObjs []client.Object
wantClusterModuleGroups []string
wantErr bool
workerAntiAffinity bool
}{
{
name: "create VirtualMachinesetResourcePolicy for control-plane only on None mode (WorkerAntiAffinity: false)",
cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"}},
vSphereCluster: &vmwarev1.VSphereCluster{Spec: vmwarev1.VSphereClusterSpec{Placement: &vmwarev1.VSphereClusterPlacement{WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: vmwarev1.VSphereClusterWorkerAntiAffinityModeNone,
}}}},
wantErr: false,
wantClusterModuleGroups: []string{ControlPlaneVMClusterModuleGroupName, getFallbackWorkerClusterModuleGroupName("cluster")},
workerAntiAffinity: false,
},
{
name: "create VirtualMachinesetResourcePolicy for control-plane only on None mode (WorkerAntiAffinity: true)",
cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"}},
vSphereCluster: &vmwarev1.VSphereCluster{Spec: vmwarev1.VSphereClusterSpec{Placement: &vmwarev1.VSphereClusterPlacement{WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: vmwarev1.VSphereClusterWorkerAntiAffinityModeNone,
}}}},
wantErr: false,
wantClusterModuleGroups: []string{ControlPlaneVMClusterModuleGroupName},
workerAntiAffinity: true,
},
{
name: "create VirtualMachinesetResourcePolicy for control-plane and workers on Cluster mode (WorkerAntiAffinity: false)",
cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"}},
vSphereCluster: &vmwarev1.VSphereCluster{Spec: vmwarev1.VSphereClusterSpec{Placement: &vmwarev1.VSphereClusterPlacement{WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster,
}}}},
wantErr: false,
wantClusterModuleGroups: []string{ControlPlaneVMClusterModuleGroupName, getFallbackWorkerClusterModuleGroupName("cluster")},
workerAntiAffinity: false,
},
{
name: "create VirtualMachinesetResourcePolicy for control-plane and workers on Cluster mode (WorkerAntiAffinity: true)",
cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"}},
vSphereCluster: &vmwarev1.VSphereCluster{Spec: vmwarev1.VSphereClusterSpec{Placement: &vmwarev1.VSphereClusterPlacement{WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster,
}}}},
wantErr: false,
wantClusterModuleGroups: []string{ClusterWorkerVMClusterModuleGroupName, ControlPlaneVMClusterModuleGroupName},
workerAntiAffinity: true,
},
{
name: "create VirtualMachinesetResourcePolicy for control-plane only when no MachineDeployments exist on MachineDeployment mode (WorkerAntiAffinity: true)",
cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"}},
vSphereCluster: &vmwarev1.VSphereCluster{Spec: vmwarev1.VSphereClusterSpec{Placement: &vmwarev1.VSphereClusterPlacement{WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment,
}}}},
wantErr: false,
wantClusterModuleGroups: []string{ControlPlaneVMClusterModuleGroupName},
workerAntiAffinity: true,
},
{
name: "create VirtualMachinesetResourcePolicy for control-plane and workers on MachineDeployment mode (WorkerAntiAffinity: true)",
cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"}},
vSphereCluster: &vmwarev1.VSphereCluster{Spec: vmwarev1.VSphereClusterSpec{Placement: &vmwarev1.VSphereClusterPlacement{WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment,
}}}},
additionalObjs: []client.Object{
&clusterv1.MachineDeployment{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "md-1", Labels: map[string]string{clusterv1.ClusterNameLabel: "cluster"}}},
&clusterv1.MachineDeployment{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "md-0", Labels: map[string]string{clusterv1.ClusterNameLabel: "cluster"}}},
&clusterv1.MachineDeployment{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "other-cluster-md-0", Labels: map[string]string{clusterv1.ClusterNameLabel: "other"}}},
},
wantErr: false,
wantClusterModuleGroups: []string{ControlPlaneVMClusterModuleGroupName, "md-0", "md-1"},
workerAntiAffinity: true,
},
{
name: "update VirtualMachinesetResourcePolicy for control-plane only on None mode and preserve used cluster modules from VirtualMachine's (WorkerAntiAffinity: true)",
cluster: &clusterv1.Cluster{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"}},
vSphereCluster: &vmwarev1.VSphereCluster{Spec: vmwarev1.VSphereClusterSpec{Placement: &vmwarev1.VSphereClusterPlacement{WorkerAntiAffinity: &vmwarev1.VSphereClusterWorkerAntiAffinity{
Mode: vmwarev1.VSphereClusterWorkerAntiAffinityModeNone,
}}}},
additionalObjs: []client.Object{
&vmoprv1.VirtualMachineSetResourcePolicy{
ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "cluster"},
},
&vmoprv1.VirtualMachine{ObjectMeta: metav1.ObjectMeta{Namespace: "some", Name: "machine-0", Labels: map[string]string{clusterv1.ClusterNameLabel: "cluster"}, Annotations: map[string]string{ClusterModuleNameAnnotationKey: "deleted-md-0"}}},
},
wantErr: false,
wantClusterModuleGroups: []string{ControlPlaneVMClusterModuleGroupName, "deleted-md-0"},
workerAntiAffinity: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)

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

s := &RPService{
Client: fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(
&vmoprv1.VirtualMachineService{},
&vmoprv1.VirtualMachine{},
).WithObjects(tt.additionalObjs...).Build(),
}
got, err := s.ReconcileResourcePolicy(ctx, tt.cluster, tt.vSphereCluster)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(got).To(BeEquivalentTo(""))
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(BeEquivalentTo(tt.cluster.Name))
}

t.Run("Creates Resource Policy using the cluster name", func(t *testing.T) {
g := NewWithT(t)
name, err := rpService.ReconcileResourcePolicy(ctx, clusterCtx)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(name).To(Equal(clusterName))

resourcePolicy, err := rpService.getVirtualMachineSetResourcePolicy(ctx, clusterCtx)
g.Expect(err).NotTo(HaveOccurred())
g.Expect(resourcePolicy.Spec.ResourcePool.Name).To(Equal(clusterName))
g.Expect(resourcePolicy.Spec.Folder).To(Equal(clusterName))
})
var resourcePolicy vmoprv1.VirtualMachineSetResourcePolicy

g.Expect(s.Client.Get(ctx, client.ObjectKey{Name: got, Namespace: tt.cluster.Namespace}, &resourcePolicy)).To(Succeed())
g.Expect(resourcePolicy.Spec.ClusterModuleGroups).To(BeEquivalentTo(tt.wantClusterModuleGroups))
})
}
}
68 changes: 61 additions & 7 deletions pkg/services/vmoperator/vmopmachine.go
Original file line number Diff line number Diff line change
@@ -38,6 +38,7 @@ import (

infrav1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
vmwarev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/vmware/v1beta1"
"sigs.k8s.io/cluster-api-provider-vsphere/feature"
capvcontext "sigs.k8s.io/cluster-api-provider-vsphere/pkg/context"
"sigs.k8s.io/cluster-api-provider-vsphere/pkg/context/vmware"
infrautilv1 "sigs.k8s.io/cluster-api-provider-vsphere/pkg/util"
@@ -432,7 +433,9 @@ func (v *VmopMachineService) reconcileVMOperatorVM(ctx context.Context, supervis
// Assign the VM's labels.
vmOperatorVM.Labels = getVMLabels(supervisorMachineCtx, vmOperatorVM.Labels)

addResourcePolicyAnnotations(supervisorMachineCtx, vmOperatorVM)
if err := addResourcePolicyAnnotations(ctx, v.Client, supervisorMachineCtx, vmOperatorVM); err != nil {
return err
}

if err := v.addVolumes(ctx, supervisorMachineCtx, vmOperatorVM); err != nil {
return err
@@ -537,7 +540,7 @@ func (v *VmopMachineService) getVirtualMachinesInCluster(ctx context.Context, su

// Helper function to add annotations to indicate which tag vm-operator should add as well as which clusterModule VM
// should be associated.
func addResourcePolicyAnnotations(supervisorMachineCtx *vmware.SupervisorMachineContext, vm *vmoprv1.VirtualMachine) {
func addResourcePolicyAnnotations(ctx context.Context, ctrlClient client.Client, supervisorMachineCtx *vmware.SupervisorMachineContext, vm *vmoprv1.VirtualMachine) error {
annotations := vm.ObjectMeta.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
@@ -548,10 +551,49 @@ func addResourcePolicyAnnotations(supervisorMachineCtx *vmware.SupervisorMachine
annotations[ClusterModuleNameAnnotationKey] = ControlPlaneVMClusterModuleGroupName
} else {
annotations[ProviderTagsAnnotationKey] = WorkerVMVMAntiAffinityTagValue
annotations[ClusterModuleNameAnnotationKey] = getMachineDeploymentNameForCluster(supervisorMachineCtx.Cluster)

// Only set the ClusterModuleGroup annotation if not already set
if _, ok := annotations[ClusterModuleNameAnnotationKey]; !ok {
var clusterModuleGroupName string
// Set ClusterModuleGroupName depending on the configured mode from VSphereCluster.Spec.Placement.WorkerAntiAffinity.Mode
// and the WorkerAntiAffinity feature-gate
switch mode := getWorkerAntiAffinityMode(supervisorMachineCtx.VSphereCluster); mode {
case vmwarev1.VSphereClusterWorkerAntiAffinityModeNone:
// Nothing to set.
case vmwarev1.VSphereClusterWorkerAntiAffinityModeCluster:
if feature.Gates.Enabled(feature.WorkerAntiAffinity) {
// Set the cluster-wide group name.
clusterModuleGroupName = ClusterWorkerVMClusterModuleGroupName
} else {
// Fallback to old name.
clusterModuleGroupName = getFallbackWorkerClusterModuleGroupName(supervisorMachineCtx.Cluster.Name)
}
case vmwarev1.VSphereClusterWorkerAntiAffinityModeMachineDeployment:
if !feature.Gates.Enabled(feature.WorkerAntiAffinity) {
// This should not be possible because MachineDeployment mode is only allowed with the feature enabled.
return errors.New("failed to set ClusterModuleGroup in mode MachineDeployment with WorkerAntiAffinity disabled")
}
// Set the MachineDeployment name as ClusterModuleGroup.
var ok bool
clusterModuleGroupName, ok = supervisorMachineCtx.Machine.Labels[clusterv1.MachineDeploymentNameLabel]
if !ok {
return errors.Errorf("failed to set ClusterModuleGroup because of missing label %s on Machine", clusterv1.MachineDeploymentNameLabel)
}
default:
return errors.Errorf("unknown mode %q configured for WorkerAntiAffinity", mode)
}

if clusterModuleGroupName != "" {
if err := checkClusterModuleGroup(ctx, ctrlClient, supervisorMachineCtx.Cluster, clusterModuleGroupName); err != nil {
return err
}
annotations[ClusterModuleNameAnnotationKey] = clusterModuleGroupName
}
}
}

vm.ObjectMeta.SetAnnotations(annotations)
return nil
}

func volumeName(machine *vmwarev1.VSphereMachine, volume vmwarev1.VSphereMachineVolume) string {
@@ -699,8 +741,20 @@ func getTopologyLabels(supervisorMachineCtx *vmware.SupervisorMachineContext) ma
return nil
}

// getMachineDeploymentName returns the MachineDeployment name for a Cluster.
// This is also the name used by VSphereMachineTemplate and KubeadmConfigTemplate.
func getMachineDeploymentNameForCluster(cluster *clusterv1.Cluster) string {
return fmt.Sprintf("%s-workers-0", cluster.Name)
func getMachineDeploymentNamesForCluster(ctx context.Context, ctrlClient client.Client, cluster *clusterv1.Cluster) ([]string, error) {
mdNames := []string{}
labels := map[string]string{clusterv1.ClusterNameLabel: cluster.GetName()}
mdList := &clusterv1.MachineDeploymentList{}
if err := ctrlClient.List(
ctx, mdList,
client.InNamespace(cluster.GetNamespace()),
client.MatchingLabels(labels)); err != nil {
return nil, errors.Wrapf(err, "failed to list MachineDeployment objects")
}
for _, md := range mdList.Items {
if md.DeletionTimestamp.IsZero() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not entirely sure we should drop deleted MD, there could still be machines for them.

mdNames = append(mdNames, md.Name)
}
}
return mdNames, nil
}
30 changes: 30 additions & 0 deletions pkg/util/cluster.go
Original file line number Diff line number Diff line change
@@ -59,6 +59,36 @@ func GetVSphereClusterFromVMwareMachine(ctx context.Context, c client.Client, ma
return vsphereCluster, err
}

// GetVMwareVSphereClusterFromMachineDeployment gets the vmware.infrastructure.cluster.x-k8s.io.VSphereCluster resource for the given MachineDeployment$.
func GetVMwareVSphereClusterFromMachineDeployment(ctx context.Context, c client.Client, machineDeployment *clusterv1.MachineDeployment) (*vmwarev1.VSphereCluster, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:
If this func is going only be used to enqueue requests

Suggested change
func GetVMwareVSphereClusterFromMachineDeployment(ctx context.Context, c client.Client, machineDeployment *clusterv1.MachineDeployment) (*vmwarev1.VSphereCluster, error) {
func MachineDeploymentToVMwareVSphereCluster(ctx context.Context, c client.Client, machineDeployment *clusterv1.MachineDeployment) (*vmwarev1.VSphereCluster, error) {
  • I will drop the final get and simply return the namespaced name

clusterName := machineDeployment.Labels[clusterv1.ClusterNameLabel]
if clusterName == "" {
return nil, errors.Errorf("error getting VSphereCluster name from MachineDeployment %s/%s",
machineDeployment.Namespace, machineDeployment.Name)
}
namespacedName := apitypes.NamespacedName{
Namespace: machineDeployment.Namespace,
Name: clusterName,
}
cluster := &clusterv1.Cluster{}
if err := c.Get(ctx, namespacedName, cluster); err != nil {
return nil, err
}

if cluster.Spec.InfrastructureRef == nil {
return nil, errors.Errorf("error getting VSphereCluster name from MachineDeployment %s/%s: Cluster.spec.infrastructureRef not yet set",
machineDeployment.Namespace, machineDeployment.Name)
}

vsphereClusterKey := apitypes.NamespacedName{
Namespace: machineDeployment.Namespace,
Name: cluster.Spec.InfrastructureRef.Name,
}
vsphereCluster := &vmwarev1.VSphereCluster{}
err := c.Get(ctx, vsphereClusterKey, vsphereCluster)
return vsphereCluster, err
}

// GetVSphereClusterFromVSphereMachine gets the infrastructure.cluster.x-k8s.io.VSphereCluster resource for the given VSphereMachine.
func GetVSphereClusterFromVSphereMachine(ctx context.Context, c client.Client, machine *infrav1.VSphereMachine) (*infrav1.VSphereCluster, error) {
clusterName := machine.Labels[clusterv1.ClusterNameLabel]