From ab7bc7e8ef3c6dbade47f14ee885f0bc1a2a05a0 Mon Sep 17 00:00:00 2001 From: ammujumdar-bcom Date: Sun, 19 Apr 2026 23:51:56 -0700 Subject: [PATCH 1/6] Add IP Ranges to VDN CRD, as well as additional CEL markers --- .../vspheredistributednetwork_types.go | 56 ++++++++++++++++++- api/v1alpha1/zz_generated.deepcopy.go | 20 +++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/api/v1alpha1/vspheredistributednetwork_types.go b/api/v1alpha1/vspheredistributednetwork_types.go index e659fc6..e13e62a 100644 --- a/api/v1alpha1/vspheredistributednetwork_types.go +++ b/api/v1alpha1/vspheredistributednetwork_types.go @@ -23,6 +23,15 @@ const ( VsphereDistributedNetworkIPPoolPressure VSphereDistributedNetworkConditionType = "IPPoolPressure" ) +type NetworkModeType string + +const ( + // NetworkModeManagement indicates the network is used for management traffic. + NetworkModeManagement NetworkModeType = "MANAGEMENT" + // NetworkModeWorkload indicates the network is used for workload traffic. + NetworkModeWorkload NetworkModeType = "WORKLOAD" +) + type IPAssignmentModeType string const ( @@ -37,6 +46,21 @@ const ( IPAssignmentModeNone IPAssignmentModeType = "none" ) +// VSphereDistributedNetworkIPRange is the static IP range for a VSphereDistributedNetwork. +type VSphereDistributedNetworkIPRange struct { + // address is the starting IPv4 address of the range. + // +kubebuilder:validation:Format=ipv4 + // +kubebuilder:validation:MinLength=7 + // +kubebuilder:validation:MaxLength=15 + // +required + Address string `json:"address,omitempty"` + + // count is the number of addresses in the range when using static range assignment. + // +kubebuilder:validation:Minimum=1 + // +required + Count int64 `json:"count,omitempty"` +} + // VSphereDistributedNetworkCondition describes the state of a VSphereDistributedNetwork at a certain point. type VSphereDistributedNetworkCondition struct { // Type is the type of VSphereDistributedNetwork condition. @@ -65,8 +89,20 @@ type VSphereDistributedNetworkCondition struct { LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" patchStrategy:"replace"` } +// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.gateway) || self.gateway == ”) : true",message="Gateway must be empty when IpAssignmentMode is dhcp or none" +// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.subnetMask) || self.subnetMask == ”) : true",message="SubnetMask must be empty when IpAssignmentMode is dhcp or none" +// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.addressRanges) || size(self.addressRanges) == 0) : true",message="AddressRanges must be empty when IpAssignmentMode is dhcp or none" +// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.ipPools) || size(self.ipPools) == 0) : true",message="IPPools must be empty when IpAssignmentMode is dhcp or none" +// +kubebuilder:validation:XValidation:rule="(!has(self.ipAssignmentMode) || self.ipAssignmentMode == 'staticpool') ? (has(self.gateway) && self.gateway != ”) : true",message="Gateway is required when IpAssignmentMode is staticpool" +// +kubebuilder:validation:XValidation:rule="(!has(self.ipAssignmentMode) || self.ipAssignmentMode == 'staticpool') ? (has(self.subnetMask) && self.subnetMask != ”) : true",message="SubnetMask is required when IpAssignmentMode is staticpool" // VSphereDistributedNetworkSpec defines the desired state of VSphereDistributedNetwork. type VSphereDistributedNetworkSpec struct { + // Mode indicates whether the network is used for MANAGEMENT or WORKLOAD traffic. + // +kubebuilder:validation:Enum=MANAGEMENT;WORKLOAD + // +kubebuilder:default:=WORKLOAD + // +optional + Mode NetworkModeType `json:"mode,omitempty"` + // PortGroupID is an existing vSphere Distributed PortGroup identifier. // //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Avoid omitempty (requiredfields wire shape). @@ -77,6 +113,9 @@ type VSphereDistributedNetworkSpec struct { // fields should be empty/unset. When using IPAssignmentModeNone, no IPv4 IP will be assigned // and no DHCP client will be configured. // Note: For IPv6 address assignment, see IPv6AssignmentMode. + // +kubebuilder:validation:Enum=dhcp;staticpool;none + // +kubebuilder:default:=staticpool + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="ipAssignmentMode is immutable" // +optional // //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Avoid pointer (optionalfields). @@ -102,16 +141,22 @@ type VSphereDistributedNetworkSpec struct { // Gateway is the gateway to use for network interfaces. This field should only be set when using // IPAssignmentModeStaticPool. For all other modes (IPAssignmentModeDHCP, IPAssignmentModeNone), this should be set // to an empty string. + // Note: The regex pattern performs IPv4 validation but also allows an empty string for backward compatibility. + // +kubebuilder:validation:Pattern="^(|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$" + // +optional // //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Avoid omitempty (requiredfields wire shape). - Gateway string `json:"gateway"` + Gateway string `json:"gateway,omitempty"` // SubnetMask is the subnet mask to use for network interfaces. This field should only be set when using // IPAssignmentModeStaticPool. For all other modes (IPAssignmentModeDHCP, IPAssignmentModeNone), this should be set // to an empty string. + // Note: The regex pattern performs IPv4 validation but also allows an empty string for backward compatibility. + // +kubebuilder:validation:Pattern="^(|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$" + // +optional // //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Avoid omitempty (requiredfields wire shape). - SubnetMask string `json:"subnetMask"` + SubnetMask string `json:"subnetMask,omitempty"` // IPv6Gateway is the IPv6 gateway to use for network interfaces. This field should only // be set when using IPv6AssignmentMode IPAssignmentModeStaticPool. For all other modes @@ -129,6 +174,12 @@ type VSphereDistributedNetworkSpec struct { // +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=128 IPv6Prefix *int32 `json:"ipv6Prefix,omitempty"` + + // addressRanges is a list of IP ranges for static IP assignment. + // +optional + // +kubebuilder:validation:MaxItems=32 + // +listType=atomic + AddressRanges []VSphereDistributedNetworkIPRange `json:"addressRanges,omitempty"` } // VLANType represents the type of VLAN configuration @@ -291,6 +342,7 @@ type VSphereDistributedNetworkStatus struct { // +genclient:nonNamespaced // +kubebuilder:object:root=true // +kubebuilder:resource:scope=Cluster +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 253 && self.metadata.name.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$')",message="metadata.name must be a lowercase RFC 1123 DNS subdomain (alphanumeric or '-' or '.', each segment starting/ending with alphanumeric; max 253 characters)" // VSphereDistributedNetwork represents schema for a network backed by a vSphere Distributed PortGroup on vSphere // Distributed switch. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 880c573..c5e5cf2 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1392,6 +1392,21 @@ func (in *VSphereDistributedNetworkCondition) DeepCopy() *VSphereDistributedNetw return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VSphereDistributedNetworkIPRange) DeepCopyInto(out *VSphereDistributedNetworkIPRange) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereDistributedNetworkIPRange. +func (in *VSphereDistributedNetworkIPRange) DeepCopy() *VSphereDistributedNetworkIPRange { + if in == nil { + return nil + } + out := new(VSphereDistributedNetworkIPRange) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VSphereDistributedNetworkList) DeepCopyInto(out *VSphereDistributedNetworkList) { *out = *in @@ -1437,6 +1452,11 @@ func (in *VSphereDistributedNetworkSpec) DeepCopyInto(out *VSphereDistributedNet *out = new(int32) **out = **in } + if in.AddressRanges != nil { + in, out := &in.AddressRanges, &out.AddressRanges + *out = make([]VSphereDistributedNetworkIPRange, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VSphereDistributedNetworkSpec. From 601fc4ae2d3a3fcc74538b7992ee9918225ae325 Mon Sep 17 00:00:00 2001 From: ammujumdar-bcom Date: Fri, 24 Apr 2026 10:41:16 -0700 Subject: [PATCH 2/6] Remove NetworkModeType and markers for PortGroupID for now --- api/v1alpha1/vspheredistributednetwork_types.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/api/v1alpha1/vspheredistributednetwork_types.go b/api/v1alpha1/vspheredistributednetwork_types.go index e13e62a..fcdc570 100644 --- a/api/v1alpha1/vspheredistributednetwork_types.go +++ b/api/v1alpha1/vspheredistributednetwork_types.go @@ -23,15 +23,6 @@ const ( VsphereDistributedNetworkIPPoolPressure VSphereDistributedNetworkConditionType = "IPPoolPressure" ) -type NetworkModeType string - -const ( - // NetworkModeManagement indicates the network is used for management traffic. - NetworkModeManagement NetworkModeType = "MANAGEMENT" - // NetworkModeWorkload indicates the network is used for workload traffic. - NetworkModeWorkload NetworkModeType = "WORKLOAD" -) - type IPAssignmentModeType string const ( @@ -97,12 +88,6 @@ type VSphereDistributedNetworkCondition struct { // +kubebuilder:validation:XValidation:rule="(!has(self.ipAssignmentMode) || self.ipAssignmentMode == 'staticpool') ? (has(self.subnetMask) && self.subnetMask != ”) : true",message="SubnetMask is required when IpAssignmentMode is staticpool" // VSphereDistributedNetworkSpec defines the desired state of VSphereDistributedNetwork. type VSphereDistributedNetworkSpec struct { - // Mode indicates whether the network is used for MANAGEMENT or WORKLOAD traffic. - // +kubebuilder:validation:Enum=MANAGEMENT;WORKLOAD - // +kubebuilder:default:=WORKLOAD - // +optional - Mode NetworkModeType `json:"mode,omitempty"` - // PortGroupID is an existing vSphere Distributed PortGroup identifier. // //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Avoid omitempty (requiredfields wire shape). From 21f122aaeef698cda423afc3cfce1e6d69f71d8c Mon Sep 17 00:00:00 2001 From: ammujumdar-bcom Date: Wed, 29 Apr 2026 13:02:11 -0700 Subject: [PATCH 3/6] Addressranges max is 1024 and allow ipv6 in address ranges --- api/v1alpha1/vspheredistributednetwork_types.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api/v1alpha1/vspheredistributednetwork_types.go b/api/v1alpha1/vspheredistributednetwork_types.go index fcdc570..6e6fd21 100644 --- a/api/v1alpha1/vspheredistributednetwork_types.go +++ b/api/v1alpha1/vspheredistributednetwork_types.go @@ -39,10 +39,10 @@ const ( // VSphereDistributedNetworkIPRange is the static IP range for a VSphereDistributedNetwork. type VSphereDistributedNetworkIPRange struct { - // address is the starting IPv4 address of the range. - // +kubebuilder:validation:Format=ipv4 - // +kubebuilder:validation:MinLength=7 - // +kubebuilder:validation:MaxLength=15 + // address is the starting IPv4 or IPv6 address of the range. + // +kubebuilder:validation:XValidation:rule="self.matches('^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$') || self.matches('^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$')",message="Address must be a valid IPv4 or IPv6 address" + // +kubebuilder:validation:MinLength=2 + // +kubebuilder:validation:MaxLength=45 // +required Address string `json:"address,omitempty"` @@ -162,7 +162,7 @@ type VSphereDistributedNetworkSpec struct { // addressRanges is a list of IP ranges for static IP assignment. // +optional - // +kubebuilder:validation:MaxItems=32 + // +kubebuilder:validation:MaxItems=1024 // +listType=atomic AddressRanges []VSphereDistributedNetworkIPRange `json:"addressRanges,omitempty"` } From d27780b52292797ecec69ec85fb25245e9bf8e84 Mon Sep 17 00:00:00 2001 From: ammujumdar-bcom Date: Wed, 29 Apr 2026 13:42:44 -0700 Subject: [PATCH 4/6] Add comment about ip pools being deleted --- api/v1alpha1/vspheredistributednetwork_types.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/vspheredistributednetwork_types.go b/api/v1alpha1/vspheredistributednetwork_types.go index 6e6fd21..5ef542d 100644 --- a/api/v1alpha1/vspheredistributednetwork_types.go +++ b/api/v1alpha1/vspheredistributednetwork_types.go @@ -118,7 +118,10 @@ type VSphereDistributedNetworkSpec struct { // IPPools references list of IPPool objects. This field should only be set when using // IPAssignmentModeStaticPool. For all other modes (IPAssignmentModeDHCP, IPAssignmentModeNone), this should be set - // to an empty list. + // to an empty list. + // When addressRanges is non-empty, the operator reconciles ipPools against those ranges: + // references that do not correspond to any address range are removed, new references are added + // where needed, and every retained reference (including ones that already matched a range) is reconciled. // //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxItems (would tighten validation). Avoid omitempty (requiredfields wire shape). IPPools []IPPoolReference `json:"ipPools"` @@ -161,6 +164,9 @@ type VSphereDistributedNetworkSpec struct { IPv6Prefix *int32 `json:"ipv6Prefix,omitempty"` // addressRanges is a list of IP ranges for static IP assignment. + // When non-empty, the operator reconciles ipPools against this list: IPPool references that do + // not map to any range here are removed, new references are added for ranges that require them, + // and all references that remain are reconciled (including those that already mapped to a range). // +optional // +kubebuilder:validation:MaxItems=1024 // +listType=atomic From 1caa9e6e1b045ee9fe14011dcfee77b677e77abd Mon Sep 17 00:00:00 2001 From: ammujumdar-bcom Date: Thu, 30 Apr 2026 11:46:47 -0700 Subject: [PATCH 5/6] Fix quotation marks --- api/v1alpha1/vspheredistributednetwork_types.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/vspheredistributednetwork_types.go b/api/v1alpha1/vspheredistributednetwork_types.go index 5ef542d..8c4b80c 100644 --- a/api/v1alpha1/vspheredistributednetwork_types.go +++ b/api/v1alpha1/vspheredistributednetwork_types.go @@ -80,12 +80,12 @@ type VSphereDistributedNetworkCondition struct { LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" patchStrategy:"replace"` } -// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.gateway) || self.gateway == ”) : true",message="Gateway must be empty when IpAssignmentMode is dhcp or none" -// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.subnetMask) || self.subnetMask == ”) : true",message="SubnetMask must be empty when IpAssignmentMode is dhcp or none" +// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.gateway) || self.gateway == '') : true",message="Gateway must be empty when IpAssignmentMode is dhcp or none" +// +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.subnetMask) || self.subnetMask == '') : true",message="SubnetMask must be empty when IpAssignmentMode is dhcp or none" // +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.addressRanges) || size(self.addressRanges) == 0) : true",message="AddressRanges must be empty when IpAssignmentMode is dhcp or none" // +kubebuilder:validation:XValidation:rule="(has(self.ipAssignmentMode) && (self.ipAssignmentMode == 'dhcp' || self.ipAssignmentMode == 'none')) ? (!has(self.ipPools) || size(self.ipPools) == 0) : true",message="IPPools must be empty when IpAssignmentMode is dhcp or none" -// +kubebuilder:validation:XValidation:rule="(!has(self.ipAssignmentMode) || self.ipAssignmentMode == 'staticpool') ? (has(self.gateway) && self.gateway != ”) : true",message="Gateway is required when IpAssignmentMode is staticpool" -// +kubebuilder:validation:XValidation:rule="(!has(self.ipAssignmentMode) || self.ipAssignmentMode == 'staticpool') ? (has(self.subnetMask) && self.subnetMask != ”) : true",message="SubnetMask is required when IpAssignmentMode is staticpool" +// +kubebuilder:validation:XValidation:rule="(!has(self.ipAssignmentMode) || self.ipAssignmentMode == 'staticpool') ? (has(self.gateway) && self.gateway != '') : true",message="Gateway is required when IpAssignmentMode is staticpool" +// +kubebuilder:validation:XValidation:rule="(!has(self.ipAssignmentMode) || self.ipAssignmentMode == 'staticpool') ? (has(self.subnetMask) && self.subnetMask != '') : true",message="SubnetMask is required when IpAssignmentMode is staticpool" // VSphereDistributedNetworkSpec defines the desired state of VSphereDistributedNetwork. type VSphereDistributedNetworkSpec struct { // PortGroupID is an existing vSphere Distributed PortGroup identifier. From f96ccd596824bc2173bb0669aef18718e94d4a02 Mon Sep 17 00:00:00 2001 From: ammujumdar-bcom Date: Mon, 4 May 2026 10:11:09 -0700 Subject: [PATCH 6/6] Provide default value for gateway and subnet mask, and remove omitempty --- api/v1alpha1/vspheredistributednetwork_types.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/v1alpha1/vspheredistributednetwork_types.go b/api/v1alpha1/vspheredistributednetwork_types.go index 8c4b80c..d577296 100644 --- a/api/v1alpha1/vspheredistributednetwork_types.go +++ b/api/v1alpha1/vspheredistributednetwork_types.go @@ -128,23 +128,25 @@ type VSphereDistributedNetworkSpec struct { // Gateway is the gateway to use for network interfaces. This field should only be set when using // IPAssignmentModeStaticPool. For all other modes (IPAssignmentModeDHCP, IPAssignmentModeNone), this should be set - // to an empty string. + // to an empty string. // Note: The regex pattern performs IPv4 validation but also allows an empty string for backward compatibility. // +kubebuilder:validation:Pattern="^(|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$" + // +kubebuilder:default:="" // +optional // - //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Avoid omitempty (requiredfields wire shape). - Gateway string `json:"gateway,omitempty"` + //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Keep stable wire shape: emit empty string rather than omitting key. + Gateway string `json:"gateway"` // SubnetMask is the subnet mask to use for network interfaces. This field should only be set when using // IPAssignmentModeStaticPool. For all other modes (IPAssignmentModeDHCP, IPAssignmentModeNone), this should be set - // to an empty string. + // to an empty string. // Note: The regex pattern performs IPv4 validation but also allows an empty string for backward compatibility. // +kubebuilder:validation:Pattern="^(|((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$" + // +kubebuilder:default:="" // +optional // - //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Avoid omitempty (requiredfields wire shape). - SubnetMask string `json:"subnetMask,omitempty"` + //nolint:kubeapilinter // Stable v1alpha1 retention: avoid MaxLength (would tighten validation). Keep stable wire shape: emit empty string rather than omitting key. + SubnetMask string `json:"subnetMask"` // IPv6Gateway is the IPv6 gateway to use for network interfaces. This field should only // be set when using IPv6AssignmentMode IPAssignmentModeStaticPool. For all other modes