From da0d172e48a57a9393feb0f88d3036d5852189ca Mon Sep 17 00:00:00 2001 From: vr4manta Date: Mon, 19 Jan 2026 15:32:24 -0500 Subject: [PATCH 1/3] Vendor updates --- go.mod | 2 + go.sum | 4 +- .../api/machine/v1beta1/types_awsprovider.go | 91 ++++++++++++++++++- .../machine/v1beta1/zz_generated.deepcopy.go | 58 +++++++++++- .../zz_generated.swagger_doc_generated.go | 29 +++++- vendor/modules.txt | 3 +- 6 files changed, 175 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index bec2870d8..b27eb11d8 100644 --- a/go.mod +++ b/go.mod @@ -367,3 +367,5 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) + +replace github.com/openshift/api => github.com/vr4manta/api v0.0.0-20260205131406-2fc32cc56240 diff --git a/go.sum b/go.sum index daef2f2bf..558303fcd 100644 --- a/go.sum +++ b/go.sum @@ -449,8 +449,6 @@ github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jD github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/openshift-eng/openshift-tests-extension v0.0.0-20251105193959-75a0be5d9bd7 h1:Z1swlS6b3Adm6RPhjqefs3DWnNFLDxRX+WC8GMXhja4= github.com/openshift-eng/openshift-tests-extension v0.0.0-20251105193959-75a0be5d9bd7/go.mod h1:6gkP5f2HL0meusT0Aim8icAspcD1cG055xxBZ9yC68M= -github.com/openshift/api v0.0.0-20260114133223-6ab113cb7368 h1:kSr3DOlq0NCrHd65HB2o/pBsks7AfRm+fkpf9RLUPoc= -github.com/openshift/api v0.0.0-20260114133223-6ab113cb7368/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= github.com/openshift/client-go v0.0.0-20251202151200-fb4471581cf8 h1:97rgISdT4IOmXlmEUV5Wr6d8BzzjPclzAjCARLbSlT0= github.com/openshift/client-go v0.0.0-20251202151200-fb4471581cf8/go.mod h1:WVJnsrbSO1J8x8KceOmv1d5CpoN34Uzsaz1O4MIOKJI= github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20250910145856-21d03d30056d h1:+sqUThLi/lmgT5/scmmjnS6+RZFtbdxRAscNfCPyLPI= @@ -616,6 +614,8 @@ github.com/uudashr/iface v1.3.1 h1:bA51vmVx1UIhiIsQFSNq6GZ6VPTk3WNMZgRiCe9R29U= github.com/uudashr/iface v1.3.1/go.mod h1:4QvspiRd3JLPAEXBQ9AiZpLbJlrWWgRChOKDJEuQTdg= github.com/vmware/govmomi v0.52.0 h1:JyxQ1IQdllrY7PJbv2am9mRsv3p9xWlIQ66bv+XnyLw= github.com/vmware/govmomi v0.52.0/go.mod h1:Yuc9xjznU3BH0rr6g7MNS1QGvxnJlE1vOvTJ7Lx7dqI= +github.com/vr4manta/api v0.0.0-20260205131406-2fc32cc56240 h1:KdEP6WoTD5JLagASkubKu5tqKqQ8t1Q/D8i/aAOYOYQ= +github.com/vr4manta/api v0.0.0-20260205131406-2fc32cc56240/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xen0n/gosmopolitan v1.2.2 h1:/p2KTnMzwRexIW8GlKawsTWOxn7UHA+jCMF/V8HHtvU= diff --git a/vendor/github.com/openshift/api/machine/v1beta1/types_awsprovider.go b/vendor/github.com/openshift/api/machine/v1beta1/types_awsprovider.go index d1d5941fa..e3508d667 100644 --- a/vendor/github.com/openshift/api/machine/v1beta1/types_awsprovider.go +++ b/vendor/github.com/openshift/api/machine/v1beta1/types_awsprovider.go @@ -331,9 +331,16 @@ type Filter struct { // TagSpecification is the name/value pair for a tag type TagSpecification struct { - // name of the tag + // name of the tag. + // This field is required and must be a non-empty string. + // Must be between 1 and 128 characters in length. + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=128 + // +required Name string `json:"name"` - // value of the tag + // value of the tag. + // When omitted, this creates a tag with an empty string as the value. + // +optional Value string `json:"value"` } @@ -407,6 +414,26 @@ type AWSMachineProviderStatus struct { // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:"conditions,omitempty"` + // dedicatedHost tracks the dynamically allocated dedicated host. + // This field is populated when allocationStrategy is Dynamic (with or without DynamicHostAllocation). + // When omitted, this indicates that the dedicated host has not yet been allocated, or allocation is in progress. + // +optional + DedicatedHost *DedicatedHostStatus `json:"dedicatedHost,omitempty"` +} + +// DedicatedHostStatus defines the observed state of a dynamically allocated dedicated host +// associated with an AWSMachine. This struct is used to track the ID of the dedicated host. +type DedicatedHostStatus struct { + // id tracks the dynamically allocated dedicated host ID. + // This field is populated when allocationStrategy is Dynamic (with or without DynamicHostAllocation). + // The value must start with "h-" followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f). + // The use of 8 lowercase hexadecimal characters is for older legacy hosts that may not have been migrated to newer format. + // Must be either 10 or 19 characters in length. + // +kubebuilder:validation:XValidation:rule="self.matches('^h-([0-9a-f]{8}|[0-9a-f]{17})$')",message="id must start with 'h-' followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f)" + // +kubebuilder:validation:MinLength=10 + // +kubebuilder:validation:MaxLength=19 + // +required + ID string `json:"id,omitempty"` } // MarketType describes the market type of an EC2 Instance @@ -454,21 +481,77 @@ type HostAffinity string const ( // HostAffinityAnyAvailable lets the platform select any available dedicated host. + HostAffinityAnyAvailable HostAffinity = "AnyAvailable" // HostAffinityDedicatedHost requires specifying a particular host via dedicatedHost.host.hostID. HostAffinityDedicatedHost HostAffinity = "DedicatedHost" ) +// AllocationStrategy selects how a dedicated host is provided to the system for assigning to the instance. +// +kubebuilder:validation:Enum:=UserProvided;Dynamic +// +enum +type AllocationStrategy string + +const ( + // AllocationStrategyUserProvided specifies that the system should assign instances to a user-provided dedicated host. + AllocationStrategyUserProvided AllocationStrategy = "UserProvided" + + // AllocationStrategyDynamic specifies that the system should dynamically allocate a dedicated host for instances. + AllocationStrategyDynamic AllocationStrategy = "Dynamic" +) + // DedicatedHost represents the configuration for the usage of dedicated host. +// +kubebuilder:validation:XValidation:rule="self.allocationStrategy == 'UserProvided' ? has(self.id) : !has(self.id)",message="id is required when allocationStrategy is UserProvided, and forbidden otherwise" +// +kubebuilder:validation:XValidation:rule="has(self.dynamicHostAllocation) ? self.allocationStrategy == 'Dynamic' : true",message="dynamicHostAllocation is only allowed when allocationStrategy is Dynamic" +// +union type DedicatedHost struct { + // allocationStrategy specifies if the dedicated host will be provided by the admin through the id field or if the host will be dynamically allocated. + // Valid values are UserProvided and Dynamic. + // When omitted, the value defaults to "UserProvided", which requires the id field to be set. + // When allocationStrategy is set to UserProvided, an ID of the dedicated host to assign must be provided. + // When allocationStrategy is set to Dynamic, a dedicated host will be allocated and used to assign instances. + // When allocationStrategy is set to Dynamic, and dynamicHostAllocation is configured, a dedicated host will be allocated and the tags in dynamicHostAllocation will be assigned to that host. + // +optional + // +unionDiscriminator + // +default="UserProvided" + AllocationStrategy *AllocationStrategy `json:"allocationStrategy,omitempty"` + // id identifies the AWS Dedicated Host on which the instance must run. // The value must start with "h-" followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f). // The use of 8 lowercase hexadecimal characters is for older legacy hosts that may not have been migrated to newer format. // Must be either 10 or 19 characters in length. - // +kubebuilder:validation:XValidation:rule="self.matches('^h-([0-9a-f]{8}|[0-9a-f]{17})$')",message="hostID must start with 'h-' followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f)" + // This field is required when allocationStrategy is UserProvided, and forbidden otherwise. + // When omitted with allocationStrategy set to Dynamic, the platform will dynamically allocate a dedicated host. + // +kubebuilder:validation:XValidation:rule="self.matches('^h-([0-9a-f]{8}|[0-9a-f]{17})$')",message="id must start with 'h-' followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f)" // +kubebuilder:validation:MinLength=10 // +kubebuilder:validation:MaxLength=19 - // +required + // +optional + // +unionMember=UserProvided ID string `json:"id,omitempty"` + + // dynamicHostAllocation specifies tags to apply to a dynamically allocated dedicated host. + // This field is only allowed when allocationStrategy is Dynamic, and is mutually exclusive with id. + // When specified, a dedicated host will be allocated with the provided tags applied. + // When omitted (and allocationStrategy is Dynamic), a dedicated host will be allocated without any additional tags. + // +optional + // +unionMember=Dynamic + DynamicHostAllocation *DynamicHostAllocationSpec `json:"dynamicHostAllocation,omitempty"` +} + +// DynamicHostAllocationSpec defines the configuration for dynamic dedicated host allocation. +// This specification always allocates exactly one dedicated host per machine. +// At least one property must be specified when this struct is used. +// Currently only Tags are available for configuring, but in the future more configs may become available. +// +kubebuilder:validation:MinProperties=1 +type DynamicHostAllocationSpec struct { + // tags specifies a set of key-value pairs to apply to the allocated dedicated host. + // When omitted, no additional user-defined tags will be applied to the allocated host. + // A maximum of 50 tags can be specified. + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=50 + // +listType=map + // +listMapKey=name + // +optional + Tags *[]TagSpecification `json:"tags,omitempty"` } diff --git a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go index d08906c7d..63b9bb5ff 100644 --- a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.deepcopy.go @@ -152,6 +152,11 @@ func (in *AWSMachineProviderStatus) DeepCopyInto(out *AWSMachineProviderStatus) (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.DedicatedHost != nil { + in, out := &in.DedicatedHost, &out.DedicatedHost + *out = new(DedicatedHostStatus) + **out = **in + } return } @@ -512,6 +517,16 @@ func (in *DataDiskManagedDiskParameters) DeepCopy() *DataDiskManagedDiskParamete // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DedicatedHost) DeepCopyInto(out *DedicatedHost) { *out = *in + if in.AllocationStrategy != nil { + in, out := &in.AllocationStrategy, &out.AllocationStrategy + *out = new(AllocationStrategy) + **out = **in + } + if in.DynamicHostAllocation != nil { + in, out := &in.DynamicHostAllocation, &out.DynamicHostAllocation + *out = new(DynamicHostAllocationSpec) + (*in).DeepCopyInto(*out) + } return } @@ -525,6 +540,22 @@ func (in *DedicatedHost) DeepCopy() *DedicatedHost { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DedicatedHostStatus) DeepCopyInto(out *DedicatedHostStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DedicatedHostStatus. +func (in *DedicatedHostStatus) DeepCopy() *DedicatedHostStatus { + if in == nil { + return nil + } + out := new(DedicatedHostStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DiskEncryptionSetParameters) DeepCopyInto(out *DiskEncryptionSetParameters) { *out = *in @@ -557,6 +588,31 @@ func (in *DiskSettings) DeepCopy() *DiskSettings { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynamicHostAllocationSpec) DeepCopyInto(out *DynamicHostAllocationSpec) { + *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = new([]TagSpecification) + if **in != nil { + in, out := *in, *out + *out = make([]TagSpecification, len(*in)) + copy(*out, *in) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicHostAllocationSpec. +func (in *DynamicHostAllocationSpec) DeepCopy() *DynamicHostAllocationSpec { + if in == nil { + return nil + } + out := new(DynamicHostAllocationSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EBSBlockDeviceSpec) DeepCopyInto(out *EBSBlockDeviceSpec) { *out = *in @@ -935,7 +991,7 @@ func (in *HostPlacement) DeepCopyInto(out *HostPlacement) { if in.DedicatedHost != nil { in, out := &in.DedicatedHost, &out.DedicatedHost *out = new(DedicatedHost) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go index 093a40076..d2dd488f5 100644 --- a/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go +++ b/vendor/github.com/openshift/api/machine/v1beta1/zz_generated.swagger_doc_generated.go @@ -54,6 +54,7 @@ var map_AWSMachineProviderStatus = map[string]string{ "instanceId": "instanceId is the instance ID of the machine created in AWS", "instanceState": "instanceState is the state of the AWS instance for this machine", "conditions": "conditions is a set of conditions associated with the Machine to indicate errors or other status", + "dedicatedHost": "dedicatedHost tracks the dynamically allocated dedicated host. This field is populated when allocationStrategy is Dynamic (with or without DynamicHostAllocation). When omitted, this indicates that the dedicated host has not yet been allocated, or allocation is in progress.", } func (AWSMachineProviderStatus) SwaggerDoc() map[string]string { @@ -93,14 +94,34 @@ func (CPUOptions) SwaggerDoc() map[string]string { } var map_DedicatedHost = map[string]string{ - "": "DedicatedHost represents the configuration for the usage of dedicated host.", - "id": "id identifies the AWS Dedicated Host on which the instance must run. The value must start with \"h-\" followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f). The use of 8 lowercase hexadecimal characters is for older legacy hosts that may not have been migrated to newer format. Must be either 10 or 19 characters in length.", + "": "DedicatedHost represents the configuration for the usage of dedicated host.", + "allocationStrategy": "allocationStrategy specifies if the dedicated host will be provided by the admin through the id field or if the host will be dynamically allocated. Valid values are UserProvided and Dynamic. When omitted, the value defaults to \"UserProvided\", which requires the id field to be set. When allocationStrategy is set to UserProvided, an ID of the dedicated host to assign must be provided. When allocationStrategy is set to Dynamic, a dedicated host will be allocated and used to assign instances. When allocationStrategy is set to Dynamic, and dynamicHostAllocation is configured, a dedicated host will be allocated and the tags in dynamicHostAllocation will be assigned to that host.", + "id": "id identifies the AWS Dedicated Host on which the instance must run. The value must start with \"h-\" followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f). The use of 8 lowercase hexadecimal characters is for older legacy hosts that may not have been migrated to newer format. Must be either 10 or 19 characters in length. This field is required when allocationStrategy is UserProvided, and forbidden otherwise. When omitted with allocationStrategy set to Dynamic, the platform will dynamically allocate a dedicated host.", + "dynamicHostAllocation": "dynamicHostAllocation specifies tags to apply to a dynamically allocated dedicated host. This field is only allowed when allocationStrategy is Dynamic, and is mutually exclusive with id. When specified, a dedicated host will be allocated with the provided tags applied. When omitted (and allocationStrategy is Dynamic), a dedicated host will be allocated without any additional tags.", } func (DedicatedHost) SwaggerDoc() map[string]string { return map_DedicatedHost } +var map_DedicatedHostStatus = map[string]string{ + "": "DedicatedHostStatus defines the observed state of a dynamically allocated dedicated host associated with an AWSMachine. This struct is used to track the ID of the dedicated host.", + "id": "id tracks the dynamically allocated dedicated host ID. This field is populated when allocationStrategy is Dynamic (with or without DynamicHostAllocation). The value must start with \"h-\" followed by either 8 or 17 lowercase hexadecimal characters (0-9 and a-f). The use of 8 lowercase hexadecimal characters is for older legacy hosts that may not have been migrated to newer format. Must be either 10 or 19 characters in length.", +} + +func (DedicatedHostStatus) SwaggerDoc() map[string]string { + return map_DedicatedHostStatus +} + +var map_DynamicHostAllocationSpec = map[string]string{ + "": "DynamicHostAllocationSpec defines the configuration for dynamic dedicated host allocation. This specification always allocates exactly one dedicated host per machine. At least one property must be specified when this struct is used. Currently only Tags are available for configuring, but in the future more configs may become available.", + "tags": "tags specifies a set of key-value pairs to apply to the allocated dedicated host. When omitted, no additional user-defined tags will be applied to the allocated host. A maximum of 50 tags can be specified.", +} + +func (DynamicHostAllocationSpec) SwaggerDoc() map[string]string { + return map_DynamicHostAllocationSpec +} + var map_EBSBlockDeviceSpec = map[string]string{ "": "EBSBlockDeviceSpec describes a block device for an EBS volume. https://docs.aws.amazon.com/goto/WebAPI/ec2-2016-11-15/EbsBlockDevice", "deleteOnTermination": "Indicates whether the EBS volume is deleted on machine termination.\n\nDeprecated: setting this field has no effect.", @@ -176,8 +197,8 @@ func (SpotMarketOptions) SwaggerDoc() map[string]string { var map_TagSpecification = map[string]string{ "": "TagSpecification is the name/value pair for a tag", - "name": "name of the tag", - "value": "value of the tag", + "name": "name of the tag. This field is required and must be a non-empty string. Must be between 1 and 128 characters in length.", + "value": "value of the tag. When omitted, this creates a tag with an empty string as the value.", } func (TagSpecification) SwaggerDoc() map[string]string { diff --git a/vendor/modules.txt b/vendor/modules.txt index 3eb1d928a..5dc690a00 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -976,7 +976,7 @@ github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo github.com/openshift-eng/openshift-tests-extension/pkg/junit github.com/openshift-eng/openshift-tests-extension/pkg/util/sets github.com/openshift-eng/openshift-tests-extension/pkg/version -# github.com/openshift/api v0.0.0-20260114133223-6ab113cb7368 +# github.com/openshift/api v0.0.0-20260114133223-6ab113cb7368 => github.com/vr4manta/api v0.0.0-20260205131406-2fc32cc56240 ## explicit; go 1.24.0 github.com/openshift/api github.com/openshift/api/annotations @@ -3668,3 +3668,4 @@ sigs.k8s.io/yaml/kyaml # k8s.io/apiserver => github.com/openshift/kubernetes/staging/src/k8s.io/apiserver v0.0.0-20251015171918-61114aa5a292 # k8s.io/kubelet => github.com/openshift/kubernetes/staging/src/k8s.io/kubelet v0.0.0-20251015171918-61114aa5a292 # k8s.io/kubernetes => github.com/openshift/kubernetes v1.30.1-0.20251027205255-4e0347881cbd +# github.com/openshift/api => github.com/vr4manta/api v0.0.0-20260205131406-2fc32cc56240 From 9db4ea6f18afcaff281e1b41f79ca0c778dd1543 Mon Sep 17 00:00:00 2001 From: vr4manta Date: Mon, 19 Jan 2026 15:34:43 -0500 Subject: [PATCH 2/3] Added AWS dynamic dedicated host support --- pkg/webhooks/machine_webhook.go | 80 +++++++- pkg/webhooks/machine_webhook_test.go | 296 ++++++++++++++++++++++++++- 2 files changed, 364 insertions(+), 12 deletions(-) diff --git a/pkg/webhooks/machine_webhook.go b/pkg/webhooks/machine_webhook.go index 38c4fc867..bd37cfa32 100644 --- a/pkg/webhooks/machine_webhook.go +++ b/pkg/webhooks/machine_webhook.go @@ -949,20 +949,15 @@ func processAWSPlacementTenancy(placement machinev1beta1.Placement) field.ErrorL switch *placement.Host.Affinity { case machinev1beta1.HostAffinityAnyAvailable: // DedicatedHost is optional. If it is set, make sure it follows conventions - if placement.Host.DedicatedHost != nil && !awsDedicatedHostNamePattern.MatchString(placement.Host.DedicatedHost.ID) { - errs = append(errs, field.Invalid(field.NewPath("spec.placement.host.dedicatedHost.id"), placement.Host.DedicatedHost.ID, "id must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)")) + if placement.Host.DedicatedHost != nil { + errs = append(errs, validateDedicatedHost(placement.Host.DedicatedHost)...) } case machinev1beta1.HostAffinityDedicatedHost: // We need to make sure DedicatedHost is set with an ID if placement.Host.DedicatedHost == nil { errs = append(errs, field.Required(field.NewPath("spec.placement.host.dedicatedHost"), "dedicatedHost is required when hostAffinity is DedicatedHost, and optional otherwise")) } else { - // If not set, return required error. If it does not match pattern, return pattern failure message. - if placement.Host.DedicatedHost.ID == "" { - errs = append(errs, field.Required(field.NewPath("spec.placement.host.dedicatedHost.id"), "id is required and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)")) - } else if !awsDedicatedHostNamePattern.MatchString(placement.Host.DedicatedHost.ID) { - errs = append(errs, field.Invalid(field.NewPath("spec.placement.host.dedicatedHost.id"), placement.Host.DedicatedHost.ID, "id must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)")) - } + errs = append(errs, validateDedicatedHost(placement.Host.DedicatedHost)...) } default: errs = append(errs, field.Invalid(field.NewPath("spec.placement.host.affinity"), placement.Host.Affinity, "hostAffinity must be either AnyAvailable or DedicatedHost")) @@ -983,6 +978,75 @@ func processAWSPlacementTenancy(placement machinev1beta1.Placement) field.ErrorL return errs } +// validateDedicatedHost validates that all fields in the DedicatedHost are configured correctly. +func validateDedicatedHost(host *machinev1beta1.DedicatedHost) field.ErrorList { + var errs field.ErrorList + + // If host is nil, then nothing to validate + if host == nil { + return errs + } + + strategy := machinev1beta1.AllocationStrategyUserProvided + if host.AllocationStrategy != nil { + strategy = *host.AllocationStrategy + } + + switch strategy { + // Empty string is for backward compatability in case an existing config exists with the allocation strategy not set. + // Default is User Provided. + case machinev1beta1.AllocationStrategyUserProvided, "": + // User Provided requires the ID being set of the host to use + if host.ID == "" { + errs = append(errs, field.Required(field.NewPath("spec.placement.host.dedicatedHost.id"), "id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)")) + } else if !awsDedicatedHostNamePattern.MatchString(host.ID) { + errs = append(errs, field.Invalid(field.NewPath("spec.placement.host.dedicatedHost.id"), host.ID, "id must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)")) + } + + // DynamicHostAllocation is not allowed if user provided + if host.DynamicHostAllocation != nil { + errs = append(errs, field.Invalid(field.NewPath("spec.placement.host.dedicatedHost.dynamicHostAllocation"), host.ID, "dynamicHostAllocation is only allowed when allocationStrategy is Dynamic")) + } + case machinev1beta1.AllocationStrategyDynamic: + // ID must not be set + if host.ID != "" { + errs = append(errs, field.Forbidden(field.NewPath("spec.placement.host.dedicatedHost.id"), "id is only allowed when allocationStrategy is Provided")) + } + + // Validate DynamicHostAllocation if present + if host.DynamicHostAllocation != nil { + // MinProperties=1: At least one property must be set + // Currently only Tags exists, so if Tags is nil, the struct is empty + if host.DynamicHostAllocation.Tags == nil { + errs = append(errs, field.Required(field.NewPath("spec.placement.host.dedicatedHost.dynamicHostAllocation.tags"), "at least one property must be specified in dynamicHostAllocation")) + } else { + tags := *host.DynamicHostAllocation.Tags + + // MinItems=1: At least 1 tag must be specified + if len(tags) < 1 { + errs = append(errs, field.Invalid(field.NewPath("spec.placement.host.dedicatedHost.dynamicHostAllocation.tags"), len(tags), "at least 1 tag must be specified")) + } + + // MaxItems=50: Maximum 50 tags can be specified + if len(tags) > 50 { + errs = append(errs, field.Invalid(field.NewPath("spec.placement.host.dedicatedHost.dynamicHostAllocation.tags"), len(tags), "maximum 50 tags can be specified")) + } + } + } + default: + errs = append( + errs, + field.Invalid( + field.NewPath("spec.placement.host.dedicatedHost.allocationStrategy"), + host.AllocationStrategy, + fmt.Sprintf("Invalid allocationStrategy, the only allowed options are: %s, %s", machinev1beta1.AllocationStrategyUserProvided, machinev1beta1.AllocationStrategyDynamic), + ), + ) + } + + return errs +} + // getDuplicatedTags iterates through the AWS TagSpecifications // to determine if any tag Name is duplicated within the list. // A list of duplicated names will be returned. diff --git a/pkg/webhooks/machine_webhook_test.go b/pkg/webhooks/machine_webhook_test.go index 087e90b62..648204115 100644 --- a/pkg/webhooks/machine_webhook_test.go +++ b/pkg/webhooks/machine_webhook_test.go @@ -376,7 +376,7 @@ func TestMachineCreation(t *testing.T) { }, }, }, - expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", }, { name: "configure host placement with AnyAvailable affinity and empty ID", @@ -399,7 +399,7 @@ func TestMachineCreation(t *testing.T) { }, }, }, - expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", }, { name: "configure host placement with AnyAvailable affinity and invalid ID", @@ -532,7 +532,7 @@ func TestMachineCreation(t *testing.T) { }, }, }, - expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", }, { name: "configure host placement with DedicatedHost affinity and ID not set", @@ -551,7 +551,7 @@ func TestMachineCreation(t *testing.T) { }, }, }, - expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", }, { name: "configure host placement with DedicatedHost affinity and invalid ID", @@ -679,6 +679,294 @@ func TestMachineCreation(t *testing.T) { }, expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host: Forbidden: host may only be specified when tenancy is 'host'", }, + { + name: "configure AllocationStrategy Provided with valid ID", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyUserProvided), + ID: "h-1234567890abcdef0", + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "configure AllocationStrategy Provided without ID", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyUserProvided), + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", + }, + { + name: "configure AllocationStrategy UserProvided with empty ID", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyUserProvided), + ID: "", + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", + }, + { + name: "configure AllocationStrategy UserProvided with DynamicHostAllocation", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyUserProvided), + ID: "h-1234567890abcdef0", + DynamicHostAllocation: &machinev1beta1.DynamicHostAllocationSpec{ + Tags: &[]machinev1beta1.TagSpecification{{Name: "key", Value: "value"}}, + }, + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.dynamicHostAllocation: Invalid value: \"h-1234567890abcdef0\": dynamicHostAllocation is only allowed when allocationStrategy is Dynamic", + }, + { + name: "configure AllocationStrategy Dynamic without ID", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyDynamic), + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "configure AllocationStrategy Dynamic with DynamicHostAllocation", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyDynamic), + DynamicHostAllocation: &machinev1beta1.DynamicHostAllocationSpec{ + Tags: &[]machinev1beta1.TagSpecification{{Name: "env", Value: "test"}}, + }, + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "configure AllocationStrategy Dynamic with DynamicHostAllocation but no Tags", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyDynamic), + DynamicHostAllocation: &machinev1beta1.DynamicHostAllocationSpec{}, + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.dynamicHostAllocation.tags: Required value: at least one property must be specified in dynamicHostAllocation", + }, + { + name: "configure AllocationStrategy Dynamic with DynamicHostAllocation with empty Tags", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyDynamic), + DynamicHostAllocation: &machinev1beta1.DynamicHostAllocationSpec{ + Tags: &[]machinev1beta1.TagSpecification{}, + }, + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.dynamicHostAllocation.tags: Invalid value: 0: at least 1 tag must be specified", + }, + { + name: "configure AllocationStrategy Dynamic with DynamicHostAllocation with more than 50 tags", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyDynamic), + DynamicHostAllocation: &machinev1beta1.DynamicHostAllocationSpec{ + Tags: &[]machinev1beta1.TagSpecification{ + {Name: "tag1", Value: "value1"}, {Name: "tag2", Value: "value2"}, {Name: "tag3", Value: "value3"}, + {Name: "tag4", Value: "value4"}, {Name: "tag5", Value: "value5"}, {Name: "tag6", Value: "value6"}, + {Name: "tag7", Value: "value7"}, {Name: "tag8", Value: "value8"}, {Name: "tag9", Value: "value9"}, + {Name: "tag10", Value: "value10"}, {Name: "tag11", Value: "value11"}, {Name: "tag12", Value: "value12"}, + {Name: "tag13", Value: "value13"}, {Name: "tag14", Value: "value14"}, {Name: "tag15", Value: "value15"}, + {Name: "tag16", Value: "value16"}, {Name: "tag17", Value: "value17"}, {Name: "tag18", Value: "value18"}, + {Name: "tag19", Value: "value19"}, {Name: "tag20", Value: "value20"}, {Name: "tag21", Value: "value21"}, + {Name: "tag22", Value: "value22"}, {Name: "tag23", Value: "value23"}, {Name: "tag24", Value: "value24"}, + {Name: "tag25", Value: "value25"}, {Name: "tag26", Value: "value26"}, {Name: "tag27", Value: "value27"}, + {Name: "tag28", Value: "value28"}, {Name: "tag29", Value: "value29"}, {Name: "tag30", Value: "value30"}, + {Name: "tag31", Value: "value31"}, {Name: "tag32", Value: "value32"}, {Name: "tag33", Value: "value33"}, + {Name: "tag34", Value: "value34"}, {Name: "tag35", Value: "value35"}, {Name: "tag36", Value: "value36"}, + {Name: "tag37", Value: "value37"}, {Name: "tag38", Value: "value38"}, {Name: "tag39", Value: "value39"}, + {Name: "tag40", Value: "value40"}, {Name: "tag41", Value: "value41"}, {Name: "tag42", Value: "value42"}, + {Name: "tag43", Value: "value43"}, {Name: "tag44", Value: "value44"}, {Name: "tag45", Value: "value45"}, + {Name: "tag46", Value: "value46"}, {Name: "tag47", Value: "value47"}, {Name: "tag48", Value: "value48"}, + {Name: "tag49", Value: "value49"}, {Name: "tag50", Value: "value50"}, {Name: "tag51", Value: "value51"}, + }, + }, + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.dynamicHostAllocation.tags: Invalid value: 51: maximum 50 tags can be specified", + }, + { + name: "configure AllocationStrategy Dynamic with ID", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: ptr.To(machinev1beta1.AllocationStrategyDynamic), + ID: "h-1234567890abcdef0", + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Forbidden: id is only allowed when allocationStrategy is Provided", + }, + { + name: "configure nil AllocationStrategy with valid ID (backward compatibility)", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: nil, + ID: "h-1234567890abcdef0", + }, + }, + }, + }, + }, + expectedError: "", + }, + { + name: "configure nil AllocationStrategy without ID (backward compatibility)", + platformType: osconfigv1.AWSPlatformType, + clusterID: "aws-cluster", + providerSpecValue: &kruntime.RawExtension{ + Object: &machinev1beta1.AWSMachineProviderConfig{ + AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")}, + InstanceType: "test", + Placement: machinev1beta1.Placement{ + Tenancy: machinev1beta1.HostTenancy, + Host: &machinev1beta1.HostPlacement{ + Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost), + DedicatedHost: &machinev1beta1.DedicatedHost{ + AllocationStrategy: nil, + }, + }, + }, + }, + }, + expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.placement.host.dedicatedHost.id: Required value: id is required when allocationStrategy is UserProvided and must start with 'h-' followed by 8 or 17 lowercase hexadecimal characters (0-9 and a-f)", + }, { name: "with VolumeType set to gp3 and Throughput set under minium value", platformType: osconfigv1.AWSPlatformType, From 80f5c909dec5808d2a95cffc8adf67ad529e0251 Mon Sep 17 00:00:00 2001 From: vr4manta Date: Fri, 6 Feb 2026 08:34:54 -0500 Subject: [PATCH 3/3] Added permissions for dynamic dedicated host support --- .../0000_30_machine-api-operator_00_credentials-request.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/0000_30_machine-api-operator_00_credentials-request.yaml b/install/0000_30_machine-api-operator_00_credentials-request.yaml index ac4dd0b2c..6cdf28399 100644 --- a/install/0000_30_machine-api-operator_00_credentials-request.yaml +++ b/install/0000_30_machine-api-operator_00_credentials-request.yaml @@ -21,6 +21,7 @@ spec: statementEntries: - effect: Allow action: + - ec2:AllocateHosts - ec2:CreateTags - ec2:DescribeAvailabilityZones - ec2:DescribeDhcpOptions @@ -32,6 +33,7 @@ spec: - ec2:DescribeRegions - ec2:DescribeSubnets - ec2:DescribeVpcs + - ec2:ReleaseHosts - ec2:RunInstances - ec2:TerminateInstances - elasticloadbalancing:DescribeLoadBalancers