From 8882ff0587139b0b58dd5321d723d0b0092a726a Mon Sep 17 00:00:00 2001 From: Ian Gann Date: Mon, 18 May 2026 15:09:28 -0700 Subject: [PATCH 1/2] Extend NamespaceNetworkConfiguration with NSX VPC networking support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds VPCConfig as the second provider-specific config block alongside VSphereDistributedConfig. VPC networking is activated by setting spec.type to "vpc" and populating spec.vpcConfig. VPCConfig supports two mutually exclusive VPC provisioning modes, enforced by CEL mutual-exclusion guards on the type: - vpc: associates a pre-existing NSX VPC by its Manager API path. - managedVPCConfig: directs Net Operator to auto-provision a new VPC. ManagedVPCConfig.project and ManagedVPCConfig.connectivityProfile correspond to nsxProject and vpcConnectivityProfile in the nsx-operator VPCNetworkConfiguration CRD [1]. Both fields are required when managedVPCConfig is set; since all fields carry omitempty (KAL convention), enforcement is via CEL rules on the type itself rather than the OpenAPI required array — an absent field is seen as "" in CEL, so self.project != '' catches the omitted case. Both modes additionally accept: - sharedSubnets: list-map of pre-existing NSX Subnets (keyed by path) to inject into each associated Namespace, with optional podDefault and vmDefault markers to designate workload-class defaults. VPCSharedSubnet.name is immutable once set (CEL oldSelf guard). - defaultSubnetSize: number of IP addresses for auto-created subnets (maximum 65536, consistent with VPCNetworkConfiguration upstream [1]). The temporary CEL restriction that limited spec.type to "vsphere-distributed" is removed. A parallel CEL guard is added for vpc: self.type == 'vpc' ? has(self.vpcConfig) : true. All new types conform to kube-api-linter without suppressions or exclusions. VPCConfig is embedded as *VPCConfig (pointer) because all its fields are optional, meaning the zero value {} is only ruled out by CEL, not by schema alone; the pointer satisfies KAL's optionalfields linter which requires a pointer when schema validation is incomplete. [1] https://github.com/vmware-tanzu/nsx-operator/blob/main/pkg/apis/vpc/v1alpha1/vpcnetworkconfiguration_types.go Co-authored-by: Cursor --- .../namespacenetworkconfiguration_types.go | 151 +++++++++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 46 ++++++ 2 files changed, 194 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/namespacenetworkconfiguration_types.go b/api/v1alpha1/namespacenetworkconfiguration_types.go index aad6426..8585022 100644 --- a/api/v1alpha1/namespacenetworkconfiguration_types.go +++ b/api/v1alpha1/namespacenetworkconfiguration_types.go @@ -85,14 +85,153 @@ type VSphereDistributedConfig struct { DefaultNetwork string `json:"defaultNetwork,omitempty"` } +// VPCSharedSubnetDefault expresses the default-network role of a shared Subnet +// for a class of workload without using a bool. Omission or null means the Subnet +// is not the default. +// +// +kubebuilder:validation:Enum=True;False +type VPCSharedSubnetDefault string + +const ( + // VPCSharedSubnetDefaultTrue marks the subnet as the default for its workload class. + VPCSharedSubnetDefaultTrue VPCSharedSubnetDefault = "True" + // VPCSharedSubnetDefaultFalse explicitly marks the subnet as not the default. + VPCSharedSubnetDefaultFalse VPCSharedSubnetDefault = "False" +) + +// VPCSharedSubnet describes a pre-existing NSX Subnet that is shared into a +// namespace. +// +// +kubebuilder:validation:XValidation:rule="oldSelf.name == '' || self.name == oldSelf.name",message="name is immutable once set" +type VPCSharedSubnet struct { + // path is the NSX Manager API path of the Subnet resource to share into + // associated namespaces. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + Path string `json:"path,omitempty"` + + // podDefault marks this subnet as the default network for Pod workloads in + // each associated namespace when set to True. At most one entry in + // sharedSubnets should set podDefault to True. + // + // +optional + PodDefault VPCSharedSubnetDefault `json:"podDefault,omitempty"` + + // vmDefault marks this subnet as the default network for VM workloads in + // each associated namespace when set to True. At most one entry in + // sharedSubnets should set vmDefault to True. + // + // +optional + VMDefault VPCSharedSubnetDefault `json:"vmDefault,omitempty"` + + // name is an optional human-readable label for this subnet entry. If + // unset, a label is derived from the path. Subnet entry names are RFC 1123 + // DNS subdomain names: lowercase alphanumeric characters, hyphens, or + // dots; each dot-separated label must start and end with an alphanumeric + // character; at most 253 characters in total. + // + // This field is immutable once set. + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` + Name string `json:"name,omitempty"` +} + +// ManagedVPCConfig specifies the parameters used when a VPC is auto-provisioned +// for each namespace associated with this NamespaceNetworkConfiguration. +// +// +kubebuilder:validation:XValidation:rule="self.project != ''",message="project is required when managedVPCConfig is set" +// +kubebuilder:validation:XValidation:rule="self.connectivityProfile != ''",message="connectivityProfile is required when managedVPCConfig is set" +type ManagedVPCConfig struct { + // project is the NSX Manager API path of the NSX Project under which the + // VPC will be auto-provisioned. Corresponds to nsxProject in + // VPCNetworkConfiguration. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + Project string `json:"project,omitempty"` + + // connectivityProfile is the NSX Manager API path of the VPC Connectivity + // Profile used to configure transit gateway attachments for the + // auto-provisioned VPC. Corresponds to vpcConnectivityProfile in + // VPCNetworkConfiguration. + // + // +required + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + ConnectivityProfile string `json:"connectivityProfile,omitempty"` + + // privateCIDRs lists the IPv4 CIDR blocks from which private subnets are + // carved for this VPC. If unset, the NSX Project defaults apply. + // + // +optional + // +kubebuilder:validation:MaxItems=16 + // +kubebuilder:validation:items:MaxLength=43 + // +listType=atomic + PrivateCIDRs []string `json:"privateCIDRs,omitempty"` +} + +// VPCConfig specifies the NSX VPC network configuration for namespaces +// associated with this NamespaceNetworkConfiguration. +// +// Exactly one of vpc or managedVPCConfig must be set. sharedSubnets and +// defaultSubnetSize apply in either mode. +// +// +kubebuilder:validation:XValidation:rule="!(has(self.vpc) && has(self.managedVPCConfig))",message="vpc and managedVPCConfig are mutually exclusive; set exactly one" +// +kubebuilder:validation:XValidation:rule="has(self.vpc) || has(self.managedVPCConfig)",message="one of vpc or managedVPCConfig must be set" +type VPCConfig struct { + // vpc is the NSX Manager API path of a pre-existing VPC to associate with + // namespaces governed by this NamespaceNetworkConfiguration. Mutually + // exclusive with managedVPCConfig. + // + // +optional + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=2048 + VPC string `json:"vpc,omitempty"` + + // managedVPCConfig specifies the parameters for a VPC that is + // auto-provisioned for each associated Namespace. + // + // The auto-provisioned VPC is lifecycle-managed by the NamespaceNetworkConfiguration. + // + // Mutually exclusive with vpc. + // + // +optional + ManagedVPCConfig ManagedVPCConfig `json:"managedVPCConfig,omitempty,omitzero"` + + // sharedSubnets lists pre-existing NSX Subnets to share into each + // associated namespace. Entries may carry podDefault or vmDefault markers + // to designate workload-class defaults. + // + // +optional + // +kubebuilder:validation:MaxItems=32 + // +listType=map + // +listMapKey=path + SharedSubnets []VPCSharedSubnet `json:"sharedSubnets,omitempty"` + + // defaultSubnetSize is the number of IP addresses in subnets + // auto-created within the VPC. If unset, the system default of 32 + // addresses (/27) applies. + // + // +optional + // +kubebuilder:validation:Maximum=65536 + DefaultSubnetSize *int64 `json:"defaultSubnetSize,omitempty"` +} + // NamespaceNetworkSpec defines the desired network configuration // for Namespaces associated with this NamespaceNetworkConfiguration. // -// The type field selects the active network provider. For the vsphere-distributed -// provider, vsphereDistributedConfig must be populated. +// The type field selects the active network provider. The corresponding +// provider-specific config section must be populated to match. // -// +kubebuilder:validation:XValidation:rule="self.type == 'vsphere-distributed'",message="only vsphere-distributed is currently supported; nsx-tier1 and vpc will be introduced in a future version" +// +kubebuilder:validation:XValidation:rule="self.type in ['vsphere-distributed', 'vpc']",message="only vsphere-distributed and vpc are currently supported; nsx-tier1 will be introduced in a future version" // +kubebuilder:validation:XValidation:rule="self.type == 'vsphere-distributed' ? (has(self.vsphereDistributedConfig.networks) && self.vsphereDistributedConfig.networks.size() > 0) : true",message="vsphereDistributedConfig.networks must contain at least one entry when type is vsphere-distributed" +// +kubebuilder:validation:XValidation:rule="self.type == 'vpc' ? has(self.vpcConfig) : true",message="vpcConfig must be set when type is vpc" type NamespaceNetworkSpec struct { // type selects the network provider for this configuration and determines // which provider-specific config section must be populated. @@ -105,6 +244,12 @@ type NamespaceNetworkSpec struct { // // +optional VSphereDistributedConfig VSphereDistributedConfig `json:"vsphereDistributedConfig,omitempty,omitzero"` + + // vpcConfig contains the NSX VPC network configuration. Required when type + // is vpc. + // + // +optional + VPCConfig *VPCConfig `json:"vpcConfig,omitempty"` } // NamespaceNetworkAssociation describes the reconciliation state of a diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 6a7b75b..50f5672 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -975,6 +975,11 @@ func (in *NamespaceNetworkConfigurationList) DeepCopyObject() runtime.Object { func (in *NamespaceNetworkSpec) DeepCopyInto(out *NamespaceNetworkSpec) { *out = *in in.VSphereDistributedConfig.DeepCopyInto(&out.VSphereDistributedConfig) + if in.VPCConfig != nil { + in, out := &in.VPCConfig, &out.VPCConfig + *out = new(VPCConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceNetworkSpec. @@ -1532,6 +1537,47 @@ func (in *VMXNET3NetworkInterfaceStatus) DeepCopy() *VMXNET3NetworkInterfaceStat return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCConfig) DeepCopyInto(out *VPCConfig) { + *out = *in + in.ManagedVPCConfig.DeepCopyInto(&out.ManagedVPCConfig) + if in.SharedSubnets != nil { + in, out := &in.SharedSubnets, &out.SharedSubnets + *out = make([]VPCSharedSubnet, len(*in)) + copy(*out, *in) + } + if in.DefaultSubnetSize != nil { + in, out := &in.DefaultSubnetSize, &out.DefaultSubnetSize + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCConfig. +func (in *VPCConfig) DeepCopy() *VPCConfig { + if in == nil { + return nil + } + out := new(VPCConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCSharedSubnet) DeepCopyInto(out *VPCSharedSubnet) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCSharedSubnet. +func (in *VPCSharedSubnet) DeepCopy() *VPCSharedSubnet { + if in == nil { + return nil + } + out := new(VPCSharedSubnet) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VSphereDistributedConfig) DeepCopyInto(out *VSphereDistributedConfig) { *out = *in From ecc0cb3b4592ea0302fe9ad2bf59cf92d4f0e579 Mon Sep 17 00:00:00 2001 From: Ian Gann Date: Mon, 1 Jun 2026 09:21:06 -0700 Subject: [PATCH 2/2] Sync to latest --- api/v1alpha1/zz_generated.deepcopy.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 50f5672..94e7e30 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -893,6 +893,26 @@ func (in *MacManagementPolicy) DeepCopy() *MacManagementPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedVPCConfig) DeepCopyInto(out *ManagedVPCConfig) { + *out = *in + if in.PrivateCIDRs != nil { + in, out := &in.PrivateCIDRs, &out.PrivateCIDRs + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedVPCConfig. +func (in *ManagedVPCConfig) DeepCopy() *ManagedVPCConfig { + if in == nil { + return nil + } + out := new(ManagedVPCConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespaceNetworkAssociation) DeepCopyInto(out *NamespaceNetworkAssociation) { *out = *in