diff --git a/api/v1alpha1/vspheredistributednetwork_types.go b/api/v1alpha1/vspheredistributednetwork_types.go index e659fc6..d577296 100644 --- a/api/v1alpha1/vspheredistributednetwork_types.go +++ b/api/v1alpha1/vspheredistributednetwork_types.go @@ -37,6 +37,21 @@ const ( IPAssignmentModeNone IPAssignmentModeType = "none" ) +// VSphereDistributedNetworkIPRange is the static IP range for a VSphereDistributedNetwork. +type VSphereDistributedNetworkIPRange struct { + // 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"` + + // 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,6 +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.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 { // PortGroupID is an existing vSphere Distributed PortGroup identifier. @@ -77,6 +98,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). @@ -94,23 +118,34 @@ 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"` // 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). + //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). + //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 @@ -129,6 +164,15 @@ 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. + // 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 + AddressRanges []VSphereDistributedNetworkIPRange `json:"addressRanges,omitempty"` } // VLANType represents the type of VLAN configuration @@ -291,6 +335,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.