Skip to content
5 changes: 5 additions & 0 deletions admission-policies/vsphere/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- unsupported-vsphere-spec-fields.yaml
213 changes: 213 additions & 0 deletions admission-policies/vsphere/unsupported-vsphere-spec-fields.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# ValidatingAdmissionPolicy for blocking unsupported vSphere fields
#
# API Version: v1beta1
# This policy targets cluster-api-provider-vsphere v1beta1 API.
# When upgrading to v1beta2, add these additional fields to block:
# - nestedHV
# - ftEncryptionMode
# - migrateEncryption
# - cryptoKeyID
# - cryptoProfile
#
# Fields Currently Allowed (conversion supports them):
# - template, cloneMode, snapshot
# - numCPUs, numCoresPerSocket, memoryMiB, diskGiB
# - server, datacenter, folder, datastore, resourcePool
# - tagIDs, dataDisks
# - network.devices[*].networkName
# - network.devices[*].gateway4 (supports IPv6 addresses too)
# - network.devices[*].ipAddrs (supports IPv4 and IPv6)
# - network.devices[*].nameservers (supports IPv4 and IPv6)
# - network.devices[*].addressesFromPools (IPAM)
#
# Fields Warned for Future Enhancement (when MAPI support is added):
# - storagePolicyName (medium priority)
# - hardwareVersion (low priority)
# - pciDevices (low priority)
# - network.devices[*].macAddr (low priority - CAPV supports it but not in MAPI yet)
#
# See docs/vsphere-field-conversion-support.md for complete field documentation.
#
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "openshift-cluster-api-unsupported-vsphere-spec-fields"
spec:
policyName: "openshift-cluster-api-unsupported-vsphere-spec-fields"
validationActions: [Deny]
matchResources:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values:
- openshift-cluster-api
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "openshift-cluster-api-unsupported-vsphere-spec-fields-warning"
spec:
policyName: "openshift-cluster-api-unsupported-vsphere-spec-fields-warning"
validationActions: [Warn]
matchResources:
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: In
values:
- openshift-cluster-api
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "openshift-cluster-api-unsupported-vsphere-spec-fields"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["infrastructure.cluster.x-k8s.io"]
apiVersions: ["v1beta1"]
operations: ["CREATE", "UPDATE"]
resources: ["vspheremachines", "vspheremachinetemplates"]
variables:
- name: machineSpec
expression: "object.kind == 'VSphereMachine' ? object.spec : object.spec.template.spec"
- name: specPath
expression: "object.kind == 'VSphereMachine' ? 'spec' : 'spec.template.spec'"
validations:
# Cluster-level configuration
- expression: "!has(variables.machineSpec.thumbprint) || variables.machineSpec.thumbprint == ''"
messageExpression: "variables.specPath + '.thumbprint is not supported - TLS validation is handled at cluster level'"

# CAPI-only runtime configuration
- expression: "!has(variables.machineSpec.customVMXKeys) || variables.machineSpec.customVMXKeys.size() == 0"
Comment thread
AnnaZivkovic marked this conversation as resolved.
messageExpression: "variables.specPath + '.customVMXKeys is not supported in OpenShift'"

# Resource management (not implemented in CAPV)
- expression: "!has(variables.machineSpec.resources)"
messageExpression: "variables.specPath + '.resources is not supported'"

# Lifecycle management (not in MAPI)
- expression: "!has(variables.machineSpec.guestSoftPowerOffTimeout)"
messageExpression: "variables.specPath + '.guestSoftPowerOffTimeout is not supported'"

- expression: "!has(variables.machineSpec.namingStrategy)"
messageExpression: "variables.specPath + '.namingStrategy is not supported'"

# Power off mode - only allow hard or unset
- expression: "!has(variables.machineSpec.powerOffMode) || variables.machineSpec.powerOffMode == '' || variables.machineSpec.powerOffMode == 'hard'"
messageExpression: "variables.specPath + '.powerOffMode must be \"hard\" or unset - soft power-off is not supported'"

# Operating system type (not used by CAPV)
- expression: "!has(variables.machineSpec.os) || variables.machineSpec.os == ''"
messageExpression: "variables.specPath + '.os is not supported'"

# Network-level fields
- expression: "!has(variables.machineSpec.network.routes) || variables.machineSpec.network.routes.size() == 0"
messageExpression: "variables.specPath + '.network.routes is not supported'"

- expression: "!has(variables.machineSpec.network.preferredAPIServerCIDR) || variables.machineSpec.network.preferredAPIServerCIDR == ''"
messageExpression: "variables.specPath + '.network.preferredAPIServerCIDR is not supported'"

# Network device fields - iterate over all devices and check unsupported fields
- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.deviceName) || device.deviceName == ''
)
messageExpression: "variables.specPath + '.network.devices[*].deviceName is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.dhcp6)
)
messageExpression: "variables.specPath + '.network.devices[*].dhcp6 is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.gateway6) || device.gateway6 == ''
)
messageExpression: "variables.specPath + '.network.devices[*].gateway6 is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.mtu)
)
messageExpression: "variables.specPath + '.network.devices[*].mtu is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.routes) || device.routes.size() == 0
)
messageExpression: "variables.specPath + '.network.devices[*].routes is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.searchDomains) || device.searchDomains.size() == 0
)
messageExpression: "variables.specPath + '.network.devices[*].searchDomains is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.dhcp4Overrides)
)
messageExpression: "variables.specPath + '.network.devices[*].dhcp4Overrides is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.dhcp6Overrides)
)
messageExpression: "variables.specPath + '.network.devices[*].dhcp6Overrides is not supported'"

- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.skipIPAllocation)
)
messageExpression: "variables.specPath + '.network.devices[*].skipIPAllocation is not supported'"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "openshift-cluster-api-unsupported-vsphere-spec-fields-warning"
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["infrastructure.cluster.x-k8s.io"]
apiVersions: ["v1beta1"]
operations: ["CREATE", "UPDATE"]
resources: ["vspheremachines", "vspheremachinetemplates"]
variables:
- name: machineSpec
expression: "object.kind == 'VSphereMachine' ? object.spec : object.spec.template.spec"
- name: specPath
expression: "object.kind == 'VSphereMachine' ? 'spec' : 'spec.template.spec'"
validations:
# Storage policy (not yet supported - future enhancement)
- expression: "!has(variables.machineSpec.storagePolicyName) || variables.machineSpec.storagePolicyName == ''"
messageExpression: "variables.specPath + '.storagePolicyName is not yet supported but planned for future release'"

# PCI devices (not yet supported - future enhancement)
- expression: "!has(variables.machineSpec.pciDevices) || variables.machineSpec.pciDevices.size() == 0"
messageExpression: "variables.specPath + '.pciDevices is not yet supported but planned for future release'"

# Hardware version (not yet supported - future enhancement)
- expression: "!has(variables.machineSpec.hardwareVersion) || variables.machineSpec.hardwareVersion == ''"
messageExpression: "variables.specPath + '.hardwareVersion is not yet supported but planned for future release'"

# MAC address (not yet supported - future enhancement)
- expression: >-
!has(variables.machineSpec.network.devices) ||
variables.machineSpec.network.devices.all(device,
!has(device.macAddr) || device.macAddr == ''
)
messageExpression: "variables.specPath + '.network.devices[*].macAddr is not yet supported but planned for future release'"
36 changes: 27 additions & 9 deletions cmd/machine-api-migration/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"k8s.io/utils/clock"
awsv1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
openstackv1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
vspherev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"

"github.com/openshift/api/features"
Expand Down Expand Up @@ -66,6 +67,7 @@ func initScheme(scheme *runtime.Scheme) {
utilruntime.Must(configv1.Install(scheme))
utilruntime.Must(awsv1.AddToScheme(scheme))
utilruntime.Must(openstackv1.AddToScheme(scheme))
utilruntime.Must(vspherev1.AddToScheme(scheme))
utilruntime.Must(clusterv1.AddToScheme(scheme))
}

Expand All @@ -91,7 +93,8 @@ func main() {
os.Exit(1)
}

if err := checkFeatureGates(ctx, log, mgr); err != nil {
currentFeatureGates, err := setupFeatureGates(ctx, log, mgr)
if err != nil {
log.Error(err, "unable to check feature gates")
os.Exit(1)
}
Expand All @@ -113,7 +116,7 @@ func main() {
os.Exit(1)
}

checkPlatformSupported(ctx, log, platform)
checkPlatformSupported(ctx, log, platform, currentFeatureGates)

for name, controller := range getControllers(operatorConfig, platform, infra, infraTypes) {
if err := controller.SetupWithManager(mgr); err != nil {
Expand All @@ -130,30 +133,45 @@ func main() {
}
}

func checkFeatureGates(ctx context.Context, log logr.Logger, mgr ctrl.Manager) error {
featureGateAccessor, err := getFeatureGates(ctx, mgr)
func setupFeatureGates(ctx context.Context, log logr.Logger, mgr ctrl.Manager) (featuregates.FeatureGate, error) {
featureGateAccessor, err := getFeatureGatesAccessor(ctx, mgr)
if err != nil {
return fmt.Errorf("unable to get feature gates: %w", err)
return nil, fmt.Errorf("unable to get feature gates: %w", err)
}

currentFeatureGates, err := featureGateAccessor.CurrentFeatureGates()
if err != nil {
return fmt.Errorf("unable to get current feature gates: %w", err)
return nil, fmt.Errorf("unable to get current feature gates: %w", err)
}

if !currentFeatureGates.Enabled(features.FeatureGateMachineAPIMigration) {
log.Info("MachineAPIMigration feature gate is not enabled, nothing to do. Waiting for termination signal.")
exitAfterTerminationSignal(ctx)
}

return nil
checkMachineAPIMigrationEnabled(ctx, log, currentFeatureGates)

return currentFeatureGates, nil
}

func checkMachineAPIMigrationEnabled(ctx context.Context, log logr.Logger, currentFeatureGates featuregates.FeatureGate) {
if !currentFeatureGates.Enabled(features.FeatureGateMachineAPIMigration) {
log.Info("MachineAPIMigration feature gate is not enabled, nothing to do. Waiting for termination signal.")
exitAfterTerminationSignal(ctx)
}
}

func checkPlatformSupported(ctx context.Context, log logr.Logger, platform configv1.PlatformType) {
func checkPlatformSupported(ctx context.Context, log logr.Logger, platform configv1.PlatformType, currentFeatureGates featuregates.FeatureGate) {
switch platform {
case configv1.AWSPlatformType, configv1.OpenStackPlatformType:
log.Info("starting controllers", "platform", platform)
case configv1.VSpherePlatformType:
if !currentFeatureGates.Enabled(features.FeatureGateMachineAPIMigrationVSphere) {
log.Info("MachineAPIMigrationVSphere feature gate is not enabled for vSphere platform. Waiting for termination signal.")
exitAfterTerminationSignal(ctx)
}

log.Info("MachineAPIMigration: starting %s controllers", platform)
default:
log.Info("MachineAPIMigration not implemented for platform, nothing to do. Waiting for termination signal.", "platform", platform)
exitAfterTerminationSignal(ctx)
Expand Down Expand Up @@ -213,7 +231,7 @@ func exitAfterTerminationSignal(ctx context.Context) {

// getFeatureGates is used to fetch the current feature gates from the cluster.
// We use this to check if the machine api migration is actually enabled or not.
func getFeatureGates(ctx context.Context, mgr ctrl.Manager) (featuregates.FeatureGateAccess, error) {
func getFeatureGatesAccessor(ctx context.Context, mgr ctrl.Manager) (featuregates.FeatureGateAccess, error) {
desiredVersion := util.GetReleaseVersion()
missingVersion := "0.0.1-snapshot"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (r *MachineMigrationReconciler) isOldAuthoritativeResourcePaused(ctx contex
return false, fmt.Errorf("failed to get Cluster API infra machine: %w", err)
}

infraMachinePausedConditionStatus, err := util.GetConditionStatus(infraMachine, clusterv1.PausedCondition)
infraMachinePausedConditionStatus, err := util.GetConditionStatusFromInfraObject(infraMachine, clusterv1.PausedCondition)
if err != nil {
return false, fmt.Errorf("unable to get paused condition for %s/%s: %w", infraMachine.GetNamespace(), infraMachine.GetName(), err)
}
Expand Down
26 changes: 26 additions & 0 deletions pkg/controllers/machinesetsync/machineset_sync_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import (
awsv1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
ibmpowervsv1 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2"
openstackv1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1"
vspherev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1"
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
"sigs.k8s.io/cluster-api/util/annotations"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -391,6 +392,12 @@ func filterOutdatedInfraMachineTemplates(infraMachineTemplateList client.ObjectL
outdatedTemplates = append(outdatedTemplates, &template)
}
}
case *vspherev1.VSphereMachineTemplateList:
for _, template := range list.Items {
if template.GetName() != newInfraMachineTemplateName {
outdatedTemplates = append(outdatedTemplates, &template)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
default:
return nil, fmt.Errorf("%w: got unknown type %T", errUnexpectedInfraMachineTemplateListType, list)
}
Expand Down Expand Up @@ -705,6 +712,20 @@ func (r *MachineSetSyncReconciler) convertCAPIToMAPIMachineSet(capiMachineSet *c
return capi2mapi.FromMachineSetAndPowerVSMachineTemplateAndPowerVSCluster( //nolint: wrapcheck
capiMachineSet, machineTemplate, cluster,
).ToMachineSet()
case configv1.VSpherePlatformType:
machineTemplate, ok := infraMachineTemplate.(*vspherev1.VSphereMachineTemplate)
if !ok {
return nil, nil, fmt.Errorf("%w, expected VSphereMachineTemplate, got %T", errUnexpectedInfraMachineTemplateType, infraMachineTemplate)
}

cluster, ok := infraCluster.(*vspherev1.VSphereCluster)
if !ok {
return nil, nil, fmt.Errorf("%w, expected VSphereCluster, got %T", errUnexpectedInfraClusterType, infraCluster)
}

return capi2mapi.FromMachineSetAndVSphereMachineTemplateAndVSphereCluster( //nolint: wrapcheck
capiMachineSet, machineTemplate, cluster,
).ToMachineSet()
default:
return nil, nil, fmt.Errorf("%w: %s", errPlatformNotSupported, r.Platform)
}
Expand All @@ -719,6 +740,8 @@ func (r *MachineSetSyncReconciler) convertMAPIToCAPIMachineSet(mapiMachineSet *m
return mapi2capi.FromOpenStackMachineSetAndInfra(mapiMachineSet, r.Infra).ToMachineSetAndMachineTemplate() //nolint:wrapcheck
case configv1.PowerVSPlatformType:
return mapi2capi.FromPowerVSMachineSetAndInfra(mapiMachineSet, r.Infra).ToMachineSetAndMachineTemplate() //nolint:wrapcheck
case configv1.VSpherePlatformType:
return mapi2capi.FromVSphereMachineSetAndInfra(mapiMachineSet, r.Infra).ToMachineSetAndMachineTemplate() //nolint:wrapcheck
default:
return nil, nil, nil, fmt.Errorf("%w: %s", errPlatformNotSupported, r.Platform)
}
Expand Down Expand Up @@ -1321,6 +1344,8 @@ func initInfraMachineTemplateListAndInfraClusterListFromProvider(platform config
return &openstackv1.OpenStackMachineTemplateList{}, &openstackv1.OpenStackClusterList{}, nil
case configv1.PowerVSPlatformType:
return &ibmpowervsv1.IBMPowerVSMachineTemplateList{}, &ibmpowervsv1.IBMPowerVSClusterList{}, nil
case configv1.VSpherePlatformType:
return &vspherev1.VSphereMachineTemplateList{}, &vspherev1.VSphereClusterList{}, nil
default:
return nil, nil, fmt.Errorf("%w: %s", errPlatformNotSupported, platform)
}
Expand All @@ -1334,6 +1359,7 @@ func compareCAPIInfraMachineTemplates(platform configv1.PlatformType, infraMachi
case configv1.AWSPlatformType:
case configv1.OpenStackPlatformType:
case configv1.PowerVSPlatformType:
case configv1.VSpherePlatformType:
default:
return nil, fmt.Errorf("%w: %s", errPlatformNotSupported, platform)
}
Expand Down
Loading