diff --git a/api/hypershift/v1beta1/aws.go b/api/hypershift/v1beta1/aws.go index 93effa553e8a..fd4b92a19791 100644 --- a/api/hypershift/v1beta1/aws.go +++ b/api/hypershift/v1beta1/aws.go @@ -75,6 +75,15 @@ type AWSNodePoolPlatform struct { // // +optional Placement *PlacementOptions `json:"placement,omitempty"` + + // cpuOptions specifies CPU configuration for EC2 instances. + // Supported on C8i, M8i, and R8i instance families. + // When omitted, AWS defaults are used (nested virtualization is not enabled). + // To revert to default behavior after setting cpuOptions, remove the entire + // cpuOptions field rather than clearing individual sub-fields. + // + // +optional + CPUOptions CPUOptions `json:"cpuOptions,omitzero"` } // PlacementOptions specifies the placement options for the EC2 instances. @@ -176,6 +185,31 @@ const ( AWSResourceTagOverridePolicyDeny AWSResourceTagOverridePolicy = "Deny" ) +// CPUOptions specifies CPU configuration for EC2 instances. +// At least one field must be specified when cpuOptions is present. +// +// +kubebuilder:validation:MinProperties=1 +type CPUOptions struct { + // nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + // Supported on C8i, M8i, and R8i instance families. + // When omitted, nested virtualization is not enabled (AWS default behavior). + // + // +optional + // +kubebuilder:validation:Enum=Enabled;Disabled + NestedVirtualizationPolicy NestedVirtualizationPolicy `json:"nestedVirtualizationPolicy,omitempty"` +} + +// NestedVirtualizationPolicy indicates whether nested virtualization is enabled or disabled. +type NestedVirtualizationPolicy string + +const ( + // NestedVirtualizationEnabled enables nested virtualization on the instance. + NestedVirtualizationEnabled NestedVirtualizationPolicy = "Enabled" + + // NestedVirtualizationDisabled disables nested virtualization on the instance. + NestedVirtualizationDisabled NestedVirtualizationPolicy = "Disabled" +) + // MarketType describes the market type for EC2 instances. type MarketType string diff --git a/api/hypershift/v1beta1/nodepool_types_test.go b/api/hypershift/v1beta1/nodepool_types_test.go index 1db4cc7ec75c..1878a8f3c561 100644 --- a/api/hypershift/v1beta1/nodepool_types_test.go +++ b/api/hypershift/v1beta1/nodepool_types_test.go @@ -17,6 +17,13 @@ type nodePoolAutoScalingNMinus1 struct { Max int32 `json:"max"` } +type awsNodePoolPlatformNMinus1 struct { + // instanceType is the EC2 instance type. + InstanceType string `json:"instanceType"` //nolint:kubeapilinter // test-only N-1 compat struct + // subnet is the subnet reference. + Subnet AWSResourceReference `json:"subnet"` //nolint:kubeapilinter // test-only N-1 compat struct +} + func TestNodePoolAutoScalingSerializationCompatibility(t *testing.T) { tests := []struct { name string @@ -296,3 +303,92 @@ func TestAWSEndpointServiceResourceTagSerializationCompatibility(t *testing.T) { }) } } + +func TestAWSNodePoolPlatformSerializationCompatibility(t *testing.T) { + tests := []struct { + name string + // current is the N (current) version of the struct + current AWSNodePoolPlatform + // expectedJSON is the expected JSON output from marshalling current + expectedJSON string + // nMinus1Result is the expected result when unmarshalling into the N-1 struct + nMinus1Result awsNodePoolPlatformNMinus1 + }{ + { + name: "When cpuOptions are set it should round-trip to N-1", + current: AWSNodePoolPlatform{ + InstanceType: "m6i.large", + Subnet: AWSResourceReference{ + ID: ptr.To("subnet-1234567890abcdef0"), + }, + CPUOptions: CPUOptions{ + NestedVirtualizationPolicy: NestedVirtualizationEnabled, + }, + }, + expectedJSON: `{"instanceType":"m6i.large","subnet":{"id":"subnet-1234567890abcdef0"},"cpuOptions":{"nestedVirtualizationPolicy":"Enabled"}}`, + nMinus1Result: awsNodePoolPlatformNMinus1{ + InstanceType: "m6i.large", + Subnet: AWSResourceReference{ + ID: ptr.To("subnet-1234567890abcdef0"), + }, + }, + }, + { + name: "When cpuOptions are omitted it should preserve N-1 JSON shape", + current: AWSNodePoolPlatform{ + InstanceType: "m6i.large", + Subnet: AWSResourceReference{ + ID: ptr.To("subnet-1234567890abcdef0"), + }, + }, + expectedJSON: `{"instanceType":"m6i.large","subnet":{"id":"subnet-1234567890abcdef0"}}`, + nMinus1Result: awsNodePoolPlatformNMinus1{ + InstanceType: "m6i.large", + Subnet: AWSResourceReference{ + ID: ptr.To("subnet-1234567890abcdef0"), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.current) + if err != nil { + t.Fatalf("failed to marshal current struct: %v", err) + } + if string(data) != tt.expectedJSON { + t.Errorf("unexpected JSON output: got %s, want %s", string(data), tt.expectedJSON) + } + + var nMinus1 awsNodePoolPlatformNMinus1 + if err := json.Unmarshal(data, &nMinus1); err != nil { + t.Fatalf("N-1 failed to unmarshal JSON from N: %v", err) + } + if nMinus1.InstanceType != tt.nMinus1Result.InstanceType { + t.Errorf("N-1 instanceType mismatch: got %s, want %s", nMinus1.InstanceType, tt.nMinus1Result.InstanceType) + } + if ptr.Deref(nMinus1.Subnet.ID, "") != ptr.Deref(tt.nMinus1Result.Subnet.ID, "") { + t.Errorf("N-1 subnet ID mismatch: got %q, want %q", ptr.Deref(nMinus1.Subnet.ID, ""), ptr.Deref(tt.nMinus1Result.Subnet.ID, "")) + } + + nMinus1Data, err := json.Marshal(tt.nMinus1Result) + if err != nil { + t.Fatalf("failed to marshal N-1 struct: %v", err) + } + var roundTripped AWSNodePoolPlatform + if err := json.Unmarshal(nMinus1Data, &roundTripped); err != nil { + t.Fatalf("N failed to unmarshal JSON from N-1: %v", err) + } + if roundTripped.InstanceType != tt.nMinus1Result.InstanceType { + t.Errorf("InstanceType mismatch after N-1 round-trip: got %s, want %s", roundTripped.InstanceType, tt.nMinus1Result.InstanceType) + } + if ptr.Deref(roundTripped.Subnet.ID, "") != ptr.Deref(tt.nMinus1Result.Subnet.ID, "") { + t.Errorf("Subnet ID mismatch after N-1 round-trip: got %q, want %q", ptr.Deref(roundTripped.Subnet.ID, ""), ptr.Deref(tt.nMinus1Result.Subnet.ID, "")) + } + if roundTripped.CPUOptions != (CPUOptions{}) { + t.Errorf("CPUOptions mismatch after N-1 round-trip: got %+v, want zero value", roundTripped.CPUOptions) + } + }) + } +} diff --git a/api/hypershift/v1beta1/zz_generated.deepcopy.go b/api/hypershift/v1beta1/zz_generated.deepcopy.go index 702745ff3476..ba1a014292df 100644 --- a/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -350,6 +350,7 @@ func (in *AWSNodePoolPlatform) DeepCopyInto(out *AWSNodePoolPlatform) { *out = new(PlacementOptions) (*in).DeepCopyInto(*out) } + out.CPUOptions = in.CPUOptions } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSNodePoolPlatform. @@ -1050,6 +1051,21 @@ func (in *AzureWorkloadIdentities) DeepCopy() *AzureWorkloadIdentities { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CPUOptions) DeepCopyInto(out *CPUOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CPUOptions. +func (in *CPUOptions) DeepCopy() *CPUOptions { + if in == nil { + return nil + } + out := new(CPUOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Capabilities) DeepCopyInto(out *Capabilities) { *out = *in diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/AAA_ungated.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/AAA_ungated.yaml index 2dd3ed7489bd..97565a58cc68 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/AAA_ungated.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/AAA_ungated.yaml @@ -409,6 +409,25 @@ spec: is chosen based on the NodePool release payload image. maxLength: 255 type: string + cpuOptions: + description: |- + cpuOptions specifies CPU configuration for EC2 instances. + Supported on C8i, M8i, and R8i instance families. + When omitted, AWS defaults are used (nested virtualization is not enabled). + To revert to default behavior after setting cpuOptions, remove the entire + cpuOptions field rather than clearing individual sub-fields. + minProperties: 1 + properties: + nestedVirtualizationPolicy: + description: |- + nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + Supported on C8i, M8i, and R8i instance families. + When omitted, nested virtualization is not enabled (AWS default behavior). + enum: + - Enabled + - Disabled + type: string + type: object imageType: description: |- imageType specifies the type of image to use for node instances. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/GCPPlatform.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/GCPPlatform.yaml index 340287f3a459..bf13108a4f11 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/GCPPlatform.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/GCPPlatform.yaml @@ -409,6 +409,25 @@ spec: is chosen based on the NodePool release payload image. maxLength: 255 type: string + cpuOptions: + description: |- + cpuOptions specifies CPU configuration for EC2 instances. + Supported on C8i, M8i, and R8i instance families. + When omitted, AWS defaults are used (nested virtualization is not enabled). + To revert to default behavior after setting cpuOptions, remove the entire + cpuOptions field rather than clearing individual sub-fields. + minProperties: 1 + properties: + nestedVirtualizationPolicy: + description: |- + nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + Supported on C8i, M8i, and R8i instance families. + When omitted, nested virtualization is not enabled (AWS default behavior). + enum: + - Enabled + - Disabled + type: string + type: object imageType: description: |- imageType specifies the type of image to use for node instances. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OSStreams.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OSStreams.yaml index d931fdf9fa01..232927b25672 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OSStreams.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OSStreams.yaml @@ -442,6 +442,25 @@ spec: is chosen based on the NodePool release payload image. maxLength: 255 type: string + cpuOptions: + description: |- + cpuOptions specifies CPU configuration for EC2 instances. + Supported on C8i, M8i, and R8i instance families. + When omitted, AWS defaults are used (nested virtualization is not enabled). + To revert to default behavior after setting cpuOptions, remove the entire + cpuOptions field rather than clearing individual sub-fields. + minProperties: 1 + properties: + nestedVirtualizationPolicy: + description: |- + nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + Supported on C8i, M8i, and R8i instance families. + When omitted, nested virtualization is not enabled (AWS default behavior). + enum: + - Enabled + - Disabled + type: string + type: object imageType: description: |- imageType specifies the type of image to use for node instances. diff --git a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OpenStack.yaml b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OpenStack.yaml index 52db786bf579..86bf70105613 100644 --- a/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OpenStack.yaml +++ b/api/hypershift/v1beta1/zz_generated.featuregated-crd-manifests/nodepools.hypershift.openshift.io/OpenStack.yaml @@ -409,6 +409,25 @@ spec: is chosen based on the NodePool release payload image. maxLength: 255 type: string + cpuOptions: + description: |- + cpuOptions specifies CPU configuration for EC2 instances. + Supported on C8i, M8i, and R8i instance families. + When omitted, AWS defaults are used (nested virtualization is not enabled). + To revert to default behavior after setting cpuOptions, remove the entire + cpuOptions field rather than clearing individual sub-fields. + minProperties: 1 + properties: + nestedVirtualizationPolicy: + description: |- + nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + Supported on C8i, M8i, and R8i instance families. + When omitted, nested virtualization is not enabled (AWS default behavior). + enum: + - Enabled + - Disabled + type: string + type: object imageType: description: |- imageType specifies the type of image to use for node instances. diff --git a/client/applyconfiguration/hypershift/v1beta1/awsnodepoolplatform.go b/client/applyconfiguration/hypershift/v1beta1/awsnodepoolplatform.go index d3e9abb2321e..a7ff0815ebe5 100644 --- a/client/applyconfiguration/hypershift/v1beta1/awsnodepoolplatform.go +++ b/client/applyconfiguration/hypershift/v1beta1/awsnodepoolplatform.go @@ -33,6 +33,7 @@ type AWSNodePoolPlatformApplyConfiguration struct { RootVolume *VolumeApplyConfiguration `json:"rootVolume,omitempty"` ResourceTags []AWSNodePoolResourceTagApplyConfiguration `json:"resourceTags,omitempty"` Placement *PlacementOptionsApplyConfiguration `json:"placement,omitempty"` + CPUOptions *CPUOptionsApplyConfiguration `json:"cpuOptions,omitempty"` } // AWSNodePoolPlatformApplyConfiguration constructs a declarative configuration of the AWSNodePoolPlatform type for use with @@ -122,3 +123,11 @@ func (b *AWSNodePoolPlatformApplyConfiguration) WithPlacement(value *PlacementOp b.Placement = value return b } + +// WithCPUOptions sets the CPUOptions field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CPUOptions field is set to the value of the last call. +func (b *AWSNodePoolPlatformApplyConfiguration) WithCPUOptions(value *CPUOptionsApplyConfiguration) *AWSNodePoolPlatformApplyConfiguration { + b.CPUOptions = value + return b +} diff --git a/client/applyconfiguration/hypershift/v1beta1/cpuoptions.go b/client/applyconfiguration/hypershift/v1beta1/cpuoptions.go new file mode 100644 index 000000000000..8108c38ccfa4 --- /dev/null +++ b/client/applyconfiguration/hypershift/v1beta1/cpuoptions.go @@ -0,0 +1,42 @@ +/* + + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +import ( + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" +) + +// CPUOptionsApplyConfiguration represents a declarative configuration of the CPUOptions type for use +// with apply. +type CPUOptionsApplyConfiguration struct { + NestedVirtualizationPolicy *hypershiftv1beta1.NestedVirtualizationPolicy `json:"nestedVirtualizationPolicy,omitempty"` +} + +// CPUOptionsApplyConfiguration constructs a declarative configuration of the CPUOptions type for use with +// apply. +func CPUOptions() *CPUOptionsApplyConfiguration { + return &CPUOptionsApplyConfiguration{} +} + +// WithNestedVirtualizationPolicy sets the NestedVirtualizationPolicy field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the NestedVirtualizationPolicy field is set to the value of the last call. +func (b *CPUOptionsApplyConfiguration) WithNestedVirtualizationPolicy(value hypershiftv1beta1.NestedVirtualizationPolicy) *CPUOptionsApplyConfiguration { + b.NestedVirtualizationPolicy = &value + return b +} diff --git a/client/applyconfiguration/utils.go b/client/applyconfiguration/utils.go index 60aa551483d5..cee870fc9336 100644 --- a/client/applyconfiguration/utils.go +++ b/client/applyconfiguration/utils.go @@ -175,6 +175,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &hypershiftv1beta1.ControlPlaneUpdateHistoryApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("ControlPlaneVersionStatus"): return &hypershiftv1beta1.ControlPlaneVersionStatusApplyConfiguration{} + case v1beta1.SchemeGroupVersion.WithKind("CPUOptions"): + return &hypershiftv1beta1.CPUOptionsApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("DataPlaneManagedIdentities"): return &hypershiftv1beta1.DataPlaneManagedIdentitiesApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("Diagnostics"): diff --git a/cmd/cluster/aws/create.go b/cmd/cluster/aws/create.go index 119329243d02..66c70188222f 100644 --- a/cmd/cluster/aws/create.go +++ b/cmd/cluster/aws/create.go @@ -511,7 +511,6 @@ func bindCoreOptions(opts *RawCreateOptions, flags *flag.FlagSet) { flags.BoolVar(&opts.PublicOnly, "public-only", opts.PublicOnly, "If true, creates a cluster that does not have private subnets or NAT gateway and assigns public IPs to all instances.") flags.BoolVar(&opts.UseROSAManagedPolicies, "use-rosa-managed-policies", opts.UseROSAManagedPolicies, "Use ROSA managed policies for the operator roles and worker instance profile") flags.BoolVar(&opts.SharedRole, "shared-role", opts.SharedRole, "Create a single shared role with all role policies instead of individual component roles") - _ = flags.MarkDeprecated("multi-arch", "Multi-arch validation is now performed automatically based on the release image and signaled in the HostedCluster.Status.PayloadArch.") } diff --git a/cmd/install/assets/crds/hypershift-operator/tests/nodepools.hypershift.openshift.io/stable.nodepools.aws.testsuite.yaml b/cmd/install/assets/crds/hypershift-operator/tests/nodepools.hypershift.openshift.io/stable.nodepools.aws.testsuite.yaml index 5b13721fed87..b8f83e1b7401 100644 --- a/cmd/install/assets/crds/hypershift-operator/tests/nodepools.hypershift.openshift.io/stable.nodepools.aws.testsuite.yaml +++ b/cmd/install/assets/crds/hypershift-operator/tests/nodepools.hypershift.openshift.io/stable.nodepools.aws.testsuite.yaml @@ -4,6 +4,112 @@ crdName: nodepools.hypershift.openshift.io version: v1beta1 tests: onCreate: + # --- AWS CPU options validation --- + - name: when nested virtualization is enabled it should pass + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: NodePool + spec: + arch: amd64 + clusterName: some-cluster + management: + autoRepair: false + upgradeType: Replace + release: + image: quay.io/openshift-release-dev/ocp-release:4.17.0-rc.0-x86_64 + replicas: 0 + platform: + aws: + instanceProfile: a-profile + instanceType: m6i.large + rootVolume: + size: 120 + type: gp3 + subnet: + id: "subnet-any" + cpuOptions: + nestedVirtualizationPolicy: Enabled + type: AWS + + - name: when nested virtualization is disabled it should pass + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: NodePool + spec: + arch: amd64 + clusterName: some-cluster + management: + autoRepair: false + upgradeType: Replace + release: + image: quay.io/openshift-release-dev/ocp-release:4.17.0-rc.0-x86_64 + replicas: 0 + platform: + aws: + instanceProfile: a-profile + instanceType: m6i.large + rootVolume: + size: 120 + type: gp3 + subnet: + id: "subnet-any" + cpuOptions: + nestedVirtualizationPolicy: Disabled + type: AWS + + - name: when cpuOptions is empty it should fail + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: NodePool + spec: + arch: amd64 + clusterName: some-cluster + management: + autoRepair: false + upgradeType: Replace + release: + image: quay.io/openshift-release-dev/ocp-release:4.17.0-rc.0-x86_64 + replicas: 0 + platform: + aws: + instanceProfile: a-profile + instanceType: m6i.large + rootVolume: + size: 120 + type: gp3 + subnet: + id: "subnet-any" + cpuOptions: {} + type: AWS + expectedError: "cpuOptions in body should have at least 1 properties" + + - name: when nested virtualization has an invalid value it should fail + initial: | + apiVersion: hypershift.openshift.io/v1beta1 + kind: NodePool + spec: + arch: amd64 + clusterName: some-cluster + management: + autoRepair: false + upgradeType: Replace + release: + image: quay.io/openshift-release-dev/ocp-release:4.17.0-rc.0-x86_64 + replicas: 0 + platform: + aws: + instanceProfile: a-profile + instanceType: m6i.large + rootVolume: + size: 120 + type: gp3 + subnet: + id: "subnet-any" + cpuOptions: + nestedVirtualizationPolicy: unsupported + type: AWS + expectedError: "Unsupported value: \"unsupported\": supported values: \"Enabled\", \"Disabled\"" + # --- AWS Placement / Capacity Reservation validation --- - name: when tenancy is host and capacity reservation is specified it should fail initial: | diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-CustomNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-CustomNoUpgrade.crd.yaml index c7223a9eddbf..da0d7452e012 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-CustomNoUpgrade.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-CustomNoUpgrade.crd.yaml @@ -445,6 +445,25 @@ spec: is chosen based on the NodePool release payload image. maxLength: 255 type: string + cpuOptions: + description: |- + cpuOptions specifies CPU configuration for EC2 instances. + Supported on C8i, M8i, and R8i instance families. + When omitted, AWS defaults are used (nested virtualization is not enabled). + To revert to default behavior after setting cpuOptions, remove the entire + cpuOptions field rather than clearing individual sub-fields. + minProperties: 1 + properties: + nestedVirtualizationPolicy: + description: |- + nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + Supported on C8i, M8i, and R8i instance families. + When omitted, nested virtualization is not enabled (AWS default behavior). + enum: + - Enabled + - Disabled + type: string + type: object imageType: description: |- imageType specifies the type of image to use for node instances. diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-Default.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-Default.crd.yaml index fcc412213e2c..3836980df5ea 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-Default.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-Default.crd.yaml @@ -412,6 +412,25 @@ spec: is chosen based on the NodePool release payload image. maxLength: 255 type: string + cpuOptions: + description: |- + cpuOptions specifies CPU configuration for EC2 instances. + Supported on C8i, M8i, and R8i instance families. + When omitted, AWS defaults are used (nested virtualization is not enabled). + To revert to default behavior after setting cpuOptions, remove the entire + cpuOptions field rather than clearing individual sub-fields. + minProperties: 1 + properties: + nestedVirtualizationPolicy: + description: |- + nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + Supported on C8i, M8i, and R8i instance families. + When omitted, nested virtualization is not enabled (AWS default behavior). + enum: + - Enabled + - Disabled + type: string + type: object imageType: description: |- imageType specifies the type of image to use for node instances. diff --git a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-TechPreviewNoUpgrade.crd.yaml b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-TechPreviewNoUpgrade.crd.yaml index 0bcefa0abd6a..8b1b94e5cf01 100644 --- a/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-TechPreviewNoUpgrade.crd.yaml +++ b/cmd/install/assets/crds/hypershift-operator/zz_generated.crd-manifests/nodepools-TechPreviewNoUpgrade.crd.yaml @@ -445,6 +445,25 @@ spec: is chosen based on the NodePool release payload image. maxLength: 255 type: string + cpuOptions: + description: |- + cpuOptions specifies CPU configuration for EC2 instances. + Supported on C8i, M8i, and R8i instance families. + When omitted, AWS defaults are used (nested virtualization is not enabled). + To revert to default behavior after setting cpuOptions, remove the entire + cpuOptions field rather than clearing individual sub-fields. + minProperties: 1 + properties: + nestedVirtualizationPolicy: + description: |- + nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + Supported on C8i, M8i, and R8i instance families. + When omitted, nested virtualization is not enabled (AWS default behavior). + enum: + - Enabled + - Disabled + type: string + type: object imageType: description: |- imageType specifies the type of image to use for node instances. diff --git a/docs/content/reference/aggregated-docs.md b/docs/content/reference/aggregated-docs.md index 1eeb3b030f55..35534c9edb21 100644 --- a/docs/content/reference/aggregated-docs.md +++ b/docs/content/reference/aggregated-docs.md @@ -41128,6 +41128,24 @@ PlacementOptions
placement specifies the placement options for the EC2 instances.
+cpuOptions,omitzero
+
+
+CPUOptions
+
+
+cpuOptions specifies CPU configuration for EC2 instances. +Supported on C8i, M8i, and R8i instance families. +When omitted, AWS defaults are used (nested virtualization is not enabled). +To revert to default behavior after setting cpuOptions, remove the entire +cpuOptions field rather than clearing individual sub-fields.
++###CPUOptions { #hypershift.openshift.io/v1beta1.CPUOptions } +
+(Appears on: +AWSNodePoolPlatform) +
++
CPUOptions specifies CPU configuration for EC2 instances. +At least one field must be specified when cpuOptions is present.
+ +| Field | +Description | +
|---|---|
+nestedVirtualizationPolicy
+
+
+NestedVirtualizationPolicy
+
+
+ |
+
+(Optional)
+ nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. +Supported on C8i, M8i, and R8i instance families. +When omitted, nested virtualization is not enabled (AWS default behavior). + |
+
(Appears on: @@ -52854,6 +52907,29 @@ which produces significantly higher metrics volume.
+(Appears on: +CPUOptions) +
++
NestedVirtualizationPolicy indicates whether nested virtualization is enabled or disabled.
+ +| Value | +Description | +
|---|---|
"Disabled" |
+NestedVirtualizationDisabled disables nested virtualization on the instance. + |
+
"Enabled" |
+NestedVirtualizationEnabled enables nested virtualization on the instance. + |
+
(Appears on: diff --git a/docs/content/reference/api.md b/docs/content/reference/api.md index 7e7407c6afc7..680bbce659fa 100644 --- a/docs/content/reference/api.md +++ b/docs/content/reference/api.md @@ -2024,6 +2024,24 @@ PlacementOptions
placement specifies the placement options for the EC2 instances.
+cpuOptions,omitzero
+
+
+CPUOptions
+
+
+cpuOptions specifies CPU configuration for EC2 instances. +Supported on C8i, M8i, and R8i instance families. +When omitted, AWS defaults are used (nested virtualization is not enabled). +To revert to default behavior after setting cpuOptions, remove the entire +cpuOptions field rather than clearing individual sub-fields.
++###CPUOptions { #hypershift.openshift.io/v1beta1.CPUOptions } +
+(Appears on: +AWSNodePoolPlatform) +
++
CPUOptions specifies CPU configuration for EC2 instances. +At least one field must be specified when cpuOptions is present.
+ +| Field | +Description | +
|---|---|
+nestedVirtualizationPolicy
+
+
+NestedVirtualizationPolicy
+
+
+ |
+
+(Optional)
+ nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. +Supported on C8i, M8i, and R8i instance families. +When omitted, nested virtualization is not enabled (AWS default behavior). + |
+
(Appears on: @@ -13750,6 +13803,29 @@ which produces significantly higher metrics volume.
+(Appears on: +CPUOptions) +
++
NestedVirtualizationPolicy indicates whether nested virtualization is enabled or disabled.
+ +| Value | +Description | +
|---|---|
"Disabled" |
+NestedVirtualizationDisabled disables nested virtualization on the instance. + |
+
"Enabled" |
+NestedVirtualizationEnabled enables nested virtualization on the instance. + |
+
(Appears on: diff --git a/hypershift-operator/controllers/nodepool/aws.go b/hypershift-operator/controllers/nodepool/aws.go index a2a6b2283854..4e846799e4ee 100644 --- a/hypershift-operator/controllers/nodepool/aws.go +++ b/hypershift-operator/controllers/nodepool/aws.go @@ -111,6 +111,7 @@ func awsMachineTemplateSpec(infraName string, hostedCluster *hyperv1.HostedClust }, } + applyAWSCPUOptions(nodePool, awsMachineTemplateSpec) applyAWSPlacementOptions(nodePool, awsMachineTemplateSpec) if hostedCluster.Annotations[hyperv1.AWSMachinePublicIPs] == "true" { @@ -206,7 +207,24 @@ func buildAWSSecurityGroups(nodePool *hyperv1.NodePool, hostedCluster *hyperv1.H return securityGroups, nil } +func applyAWSCPUOptions(nodePool *hyperv1.NodePool, spec *capiaws.AWSMachineTemplateSpec) { + if nodePool.Spec.Platform.AWS == nil { + return + } + + switch nodePool.Spec.Platform.AWS.CPUOptions.NestedVirtualizationPolicy { + case hyperv1.NestedVirtualizationEnabled: + spec.Template.Spec.CPUOptions.NestedVirtualization = capiaws.NestedVirtualizationPolicyEnabled + case hyperv1.NestedVirtualizationDisabled: + spec.Template.Spec.CPUOptions.NestedVirtualization = capiaws.NestedVirtualizationPolicyDisabled + } +} + func applyAWSPlacementOptions(nodePool *hyperv1.NodePool, spec *capiaws.AWSMachineTemplateSpec) { + if nodePool.Spec.Platform.AWS == nil { + return + } + placement := nodePool.Spec.Platform.AWS.Placement if placement == nil { return @@ -248,6 +266,7 @@ func applyAWSPlacementOptions(nodePool *hyperv1.NodePool, spec *capiaws.AWSMachi spec.Template.Spec.CapacityReservationID = capacityReservation.ID spec.Template.Spec.CapacityReservationPreference = capiaws.CapacityReservationPreference(capacityReservation.Preference) } + } func awsAdditionalTags(nodePool *hyperv1.NodePool, hostedCluster *hyperv1.HostedCluster, infraName string) capiaws.Tags { @@ -508,9 +527,39 @@ func (r NodePoolReconciler) validateAWSPlatformConfig(ctx context.Context, nodeP } } + if err := validateNestedVirtualizationInstanceType(nodePool.Spec.Platform.AWS.CPUOptions, nodePool.Spec.Platform.AWS.InstanceType); err != nil { + return err + } + return nil } +// nestedVirtualizationSupportedInstanceFamilies are the EC2 instance families that support +// CpuOptions.NestedVirtualization, per AWS's "Supported CPU options" documentation: +// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/cpu-options-supported-instances-values.html +// This includes the base family (e.g. "c8i") and its "-flex" variant (e.g. "c8i-flex"), both of +// which were made generally available together for each family. +var nestedVirtualizationSupportedInstanceFamilies = []string{"c8i", "m8i", "r8i"} + +// validateNestedVirtualizationInstanceType returns an error if cpuOptions.nestedVirtualizationPolicy +// is set on an EC2 instance type that doesn't support it. Nested virtualization is only supported +// on 8th generation Intel-based instance types (c8i, m8i, r8i, and their "-flex" variants). +func validateNestedVirtualizationInstanceType(cpuOptions hyperv1.CPUOptions, instanceType string) error { + if cpuOptions.NestedVirtualizationPolicy != hyperv1.NestedVirtualizationEnabled { + // Nothing to validate: the field is unset, or explicitly disabled (a no-op on any instance type). + return nil + } + + family, _, _ := strings.Cut(instanceType, ".") + for _, supported := range nestedVirtualizationSupportedInstanceFamilies { + if family == supported || family == supported+"-flex" { + return nil + } + } + + return fmt.Errorf("cpuOptions.nestedVirtualizationPolicy is only supported on C8i, M8i, and R8i instance families (including their -flex variants), got instanceType %q", instanceType) +} + // getWindowsAMI returns the appropriate Windows AMI for the given region from release image metadata. func getWindowsAMI(region string, specifiedArch string, releaseImage *releaseinfo.ReleaseImage) (string, error) { if releaseImage == nil { diff --git a/hypershift-operator/controllers/nodepool/aws_test.go b/hypershift-operator/controllers/nodepool/aws_test.go index 6bff1cbf502e..e6c07a75bd40 100644 --- a/hypershift-operator/controllers/nodepool/aws_test.go +++ b/hypershift-operator/controllers/nodepool/aws_test.go @@ -695,6 +695,84 @@ func TestValidateAWSPlatformConfig(t *testing.T) { } } +func TestValidateNestedVirtualizationInstanceType(t *testing.T) { + testCases := []struct { + name string + cpuOptions hyperv1.CPUOptions + instanceType string + expectedError string + }{ + { + name: "nestedVirtualizationPolicy unset, any instance type is valid", + cpuOptions: hyperv1.CPUOptions{}, + instanceType: "m5.large", + }, + { + name: "enabled on supported c8i family", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled}, + instanceType: "c8i.2xlarge", + }, + { + name: "enabled on supported c8i-flex variant", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled}, + instanceType: "c8i-flex.2xlarge", + }, + { + name: "enabled on supported m8i family", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled}, + instanceType: "m8i.4xlarge", + }, + { + name: "enabled on supported r8i-flex variant", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled}, + instanceType: "r8i-flex.xlarge", + }, + { + name: "disabled on unsupported family is still valid (explicit disable is a no-op everywhere)", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationDisabled}, + instanceType: "m5.large", + }, + { + name: "enabled on unsupported m5 family", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled}, + instanceType: "m5.large", + expectedError: "cpuOptions.nestedVirtualizationPolicy is only supported on C8i, M8i, and R8i instance families", + }, + { + name: "enabled on unsupported c7i family (previous generation)", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled}, + instanceType: "c7i.2xlarge", + expectedError: "cpuOptions.nestedVirtualizationPolicy is only supported on C8i, M8i, and R8i instance families", + }, + { + name: "enabled on unsupported AMD c8a family", + cpuOptions: hyperv1.CPUOptions{NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled}, + instanceType: "c8a.2xlarge", + expectedError: "cpuOptions.nestedVirtualizationPolicy is only supported on C8i, M8i, and R8i instance families", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := validateNestedVirtualizationInstanceType(tc.cpuOptions, tc.instanceType) + if tc.expectedError == "" { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + return + } + + if err == nil { + t.Fatalf("expected an error, got nothing") + } + + if !strings.Contains(err.Error(), tc.expectedError) { + t.Fatalf("expected error to contain %s, got %v", tc.expectedError, err) + } + }) + } +} + func TestGetWindowsAMI(t *testing.T) { testCases := []struct { name string @@ -1820,12 +1898,13 @@ func TestBuildAWSSecurityGroups(t *testing.T) { } } -func TestApplyAWSPlacementOptions(t *testing.T) { +func TestApplyAWSMachineOptions(t *testing.T) { capacityReservationID := "cr-0123456789abcdef0" testCases := []struct { name string nodePool *hyperv1.NodePool + expectedNestedVirtualization capiaws.NestedVirtualizationPolicy expectedSpotMarketOptions *capiaws.SpotMarketOptions expectedMarketType capiaws.MarketType expectedTenancy string @@ -1844,6 +1923,46 @@ func TestApplyAWSPlacementOptions(t *testing.T) { }, }, }, + { + name: "When nested virtualization is enabled, it should set CPUOptions on spec", + nodePool: &hyperv1.NodePool{ + Spec: hyperv1.NodePoolSpec{ + Platform: hyperv1.NodePoolPlatform{ + AWS: &hyperv1.AWSNodePoolPlatform{ + CPUOptions: hyperv1.CPUOptions{ + NestedVirtualizationPolicy: hyperv1.NestedVirtualizationEnabled, + }, + }, + }, + }, + }, + expectedNestedVirtualization: capiaws.NestedVirtualizationPolicyEnabled, + }, + { + name: "When nested virtualization is disabled, it should set CPUOptions on spec", + nodePool: &hyperv1.NodePool{ + Spec: hyperv1.NodePoolSpec{ + Platform: hyperv1.NodePoolPlatform{ + AWS: &hyperv1.AWSNodePoolPlatform{ + CPUOptions: hyperv1.CPUOptions{ + NestedVirtualizationPolicy: hyperv1.NestedVirtualizationDisabled, + }, + }, + }, + }, + }, + expectedNestedVirtualization: capiaws.NestedVirtualizationPolicyDisabled, + }, + { + name: "When AWS platform is nil, it should not modify spec", + nodePool: &hyperv1.NodePool{ + Spec: hyperv1.NodePoolSpec{ + Platform: hyperv1.NodePoolPlatform{ + AWS: nil, + }, + }, + }, + }, { name: "When marketType is Spot with no MaxPrice, it should set empty SpotMarketOptions", nodePool: &hyperv1.NodePool{ @@ -1988,13 +2107,15 @@ func TestApplyAWSPlacementOptions(t *testing.T) { t.Run(tc.name, func(t *testing.T) { g := NewWithT(t) spec := &capiaws.AWSMachineTemplateSpec{} + applyAWSCPUOptions(tc.nodePool, spec) applyAWSPlacementOptions(tc.nodePool, spec) - g.Expect(spec.Template.Spec.SpotMarketOptions).To(Equal(tc.expectedSpotMarketOptions)) - g.Expect(spec.Template.Spec.MarketType).To(Equal(tc.expectedMarketType)) - g.Expect(spec.Template.Spec.Tenancy).To(Equal(tc.expectedTenancy)) - g.Expect(spec.Template.Spec.CapacityReservationID).To(Equal(tc.expectedCapacityReservationID)) - g.Expect(spec.Template.Spec.CapacityReservationPreference).To(Equal(tc.expectedCapReservationPreference)) + g.Expect(spec.Template.Spec.CPUOptions.NestedVirtualization).To(Equal(tc.expectedNestedVirtualization), "CPUOptions.NestedVirtualization mismatch") + g.Expect(spec.Template.Spec.SpotMarketOptions).To(Equal(tc.expectedSpotMarketOptions), "SpotMarketOptions mismatch") + g.Expect(spec.Template.Spec.MarketType).To(Equal(tc.expectedMarketType), "MarketType mismatch") + g.Expect(spec.Template.Spec.Tenancy).To(Equal(tc.expectedTenancy), "Tenancy mismatch") + g.Expect(spec.Template.Spec.CapacityReservationID).To(Equal(tc.expectedCapacityReservationID), "CapacityReservationID mismatch") + g.Expect(spec.Template.Spec.CapacityReservationPreference).To(Equal(tc.expectedCapReservationPreference), "CapacityReservationPreference mismatch") }) } } diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/aws.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/aws.go index 93effa553e8a..fd4b92a19791 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/aws.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/aws.go @@ -75,6 +75,15 @@ type AWSNodePoolPlatform struct { // // +optional Placement *PlacementOptions `json:"placement,omitempty"` + + // cpuOptions specifies CPU configuration for EC2 instances. + // Supported on C8i, M8i, and R8i instance families. + // When omitted, AWS defaults are used (nested virtualization is not enabled). + // To revert to default behavior after setting cpuOptions, remove the entire + // cpuOptions field rather than clearing individual sub-fields. + // + // +optional + CPUOptions CPUOptions `json:"cpuOptions,omitzero"` } // PlacementOptions specifies the placement options for the EC2 instances. @@ -176,6 +185,31 @@ const ( AWSResourceTagOverridePolicyDeny AWSResourceTagOverridePolicy = "Deny" ) +// CPUOptions specifies CPU configuration for EC2 instances. +// At least one field must be specified when cpuOptions is present. +// +// +kubebuilder:validation:MinProperties=1 +type CPUOptions struct { + // nestedVirtualizationPolicy indicates whether to enable nested virtualization on the instance. + // Supported on C8i, M8i, and R8i instance families. + // When omitted, nested virtualization is not enabled (AWS default behavior). + // + // +optional + // +kubebuilder:validation:Enum=Enabled;Disabled + NestedVirtualizationPolicy NestedVirtualizationPolicy `json:"nestedVirtualizationPolicy,omitempty"` +} + +// NestedVirtualizationPolicy indicates whether nested virtualization is enabled or disabled. +type NestedVirtualizationPolicy string + +const ( + // NestedVirtualizationEnabled enables nested virtualization on the instance. + NestedVirtualizationEnabled NestedVirtualizationPolicy = "Enabled" + + // NestedVirtualizationDisabled disables nested virtualization on the instance. + NestedVirtualizationDisabled NestedVirtualizationPolicy = "Disabled" +) + // MarketType describes the market type for EC2 instances. type MarketType string diff --git a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go index 702745ff3476..ba1a014292df 100644 --- a/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/hypershift/api/hypershift/v1beta1/zz_generated.deepcopy.go @@ -350,6 +350,7 @@ func (in *AWSNodePoolPlatform) DeepCopyInto(out *AWSNodePoolPlatform) { *out = new(PlacementOptions) (*in).DeepCopyInto(*out) } + out.CPUOptions = in.CPUOptions } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSNodePoolPlatform. @@ -1050,6 +1051,21 @@ func (in *AzureWorkloadIdentities) DeepCopy() *AzureWorkloadIdentities { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CPUOptions) DeepCopyInto(out *CPUOptions) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CPUOptions. +func (in *CPUOptions) DeepCopy() *CPUOptions { + if in == nil { + return nil + } + out := new(CPUOptions) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Capabilities) DeepCopyInto(out *Capabilities) { *out = *in