Skip to content

Commit eb7c6cd

Browse files
committed
Added dedicated hosts support for AWS
1 parent 178bb01 commit eb7c6cd

File tree

6 files changed

+142
-6
lines changed

6 files changed

+142
-6
lines changed

pkg/operator/operator.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ func (optr *Operator) maoConfigFromInfrastructure() (*OperatorConfig, error) {
474474
// flags, we selectively populate the map (and therefore passed
475475
// as args)
476476
features := map[string]bool{
477+
string(apifeatures.FeatureGateAWSDedicatedHosts): featureGates.Enabled(apifeatures.FeatureGateAWSDedicatedHosts),
477478
string(apifeatures.FeatureGateMachineAPIMigration): featureGates.Enabled(apifeatures.FeatureGateMachineAPIMigration),
478479
string(apifeatures.FeatureGateAzureWorkloadIdentity): featureGates.Enabled(apifeatures.FeatureGateAzureWorkloadIdentity),
479480
string(apifeatures.FeatureGateVSphereMultiDisk): featureGates.Enabled(apifeatures.FeatureGateVSphereMultiDisk),

pkg/operator/operator_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,15 @@ var (
4545
{Name: apifeatures.FeatureGateAzureWorkloadIdentity},
4646
{Name: apifeatures.FeatureGateVSphereMultiDisk},
4747
{Name: apifeatures.FeatureGateVSphereHostVMGroupZonal},
48+
{Name: apifeatures.FeatureGateAWSDedicatedHosts},
4849
}
4950

5051
enabledFeatureMap = map[string]bool{
5152
"MachineAPIMigration": true,
5253
"AzureWorkloadIdentity": true,
5354
"VSphereMultiDisk": true,
5455
"VSphereHostVMGroupZonal": true,
56+
"AWSDedicatedHosts": true,
5557
}
5658
)
5759

pkg/operator/sync_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ func Test_ensureDependecyAnnotations(t *testing.T) {
270270
input := test.input.DeepCopy()
271271
ensureDependecyAnnotations(test.inputHashes, input)
272272
if !equality.Semantic.DeepEqual(test.expected, input) {
273-
t.Fatalf("unexpected: %s", diff.ObjectDiff(test.expected, input))
273+
t.Fatalf("unexpected: %s", diff.Diff(test.expected, input))
274274
}
275275
})
276276
}

pkg/webhooks/machine_webhook.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ type systemSpecifications struct {
5050
type machineArch string
5151

5252
var (
53+
// AWS Variables / Defaults
54+
55+
// awsDedicatedHostNamePattern is used to validate the id of a dedicated host
56+
awsDedicatedHostNamePattern = regexp.MustCompile(`^h-[0-9a-f]{17}$`)
57+
5358
// Azure Defaults
5459
defaultAzureVnet = func(clusterID string) string {
5560
return fmt.Sprintf("%s-vnet", clusterID)
@@ -897,6 +902,33 @@ func validateAWS(m *machinev1beta1.Machine, config *admissionConfig) (bool, []st
897902
}
898903
}
899904

905+
// Dedicated host support.
906+
// Check if host placement is configured. If so, then we need to determine placement affinity and validate configs.
907+
if providerSpec.HostPlacement != nil {
908+
klog.V(4).Infof("Validating AWS Host Placement")
909+
placement := *providerSpec.HostPlacement
910+
if placement.Affinity == nil {
911+
errs = append(errs, field.Required(field.NewPath("spec.hostPlacement.affinity"), "affinity is required and must be set to either AnyAvailable or DedicatedHost"))
912+
} else {
913+
switch *placement.Affinity {
914+
case machinev1beta1.HostAffinityAnyAvailable:
915+
// Cannot have DedicatedHost set
916+
if placement.DedicatedHost != nil {
917+
errs = append(errs, field.Forbidden(field.NewPath("spec.hostPlacement.dedicatedHost"), "dedicatedHost is required when affinity is DedicatedHost, and forbidden otherwise"))
918+
}
919+
case machinev1beta1.HostAffinityDedicatedHost:
920+
// We need to make sure DedicatedHost is set with a HostID
921+
if placement.DedicatedHost == nil {
922+
errs = append(errs, field.Required(field.NewPath("spec.hostPlacement.dedicatedHost"), "dedicatedHost is required when affinity is DedicatedHost, and forbidden otherwise"))
923+
} else if awsDedicatedHostNamePattern.FindStringSubmatch(placement.DedicatedHost.ID) == nil {
924+
errs = append(errs, field.Invalid(field.NewPath("spec.hostPlacement.dedicatedHost.id"), placement.DedicatedHost.ID, "id must start with 'h-' followed by 17 lowercase hexadecimal characters (0-9 and a-f)"))
925+
}
926+
default:
927+
errs = append(errs, field.Invalid(field.NewPath("spec.hostPlacement.affinity"), placement.Affinity, "affinity must be either AnyAvailable or DedicatedHost"))
928+
}
929+
}
930+
}
931+
900932
if len(errs) > 0 {
901933
return false, warnings, errs
902934
}

pkg/webhooks/machine_webhook_test.go

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,107 @@ func TestMachineCreation(t *testing.T) {
316316
},
317317
expectedError: "",
318318
},
319+
{
320+
name: "configure host placement with AnyAvailable affinity",
321+
platformType: osconfigv1.AWSPlatformType,
322+
clusterID: "aws-cluster",
323+
providerSpecValue: &kruntime.RawExtension{
324+
Object: &machinev1beta1.AWSMachineProviderConfig{
325+
AMI: machinev1beta1.AWSResourceReference{
326+
ID: ptr.To[string]("ami"),
327+
},
328+
InstanceType: "test",
329+
HostPlacement: &machinev1beta1.HostPlacement{
330+
Affinity: ptr.To(machinev1beta1.HostAffinityAnyAvailable),
331+
},
332+
},
333+
},
334+
expectedError: "",
335+
},
336+
{
337+
name: "configure host placement with invalid affinity",
338+
platformType: osconfigv1.AWSPlatformType,
339+
clusterID: "aws-cluster",
340+
providerSpecValue: &kruntime.RawExtension{
341+
Object: &machinev1beta1.AWSMachineProviderConfig{
342+
AMI: machinev1beta1.AWSResourceReference{
343+
ID: ptr.To[string]("ami"),
344+
},
345+
InstanceType: "test",
346+
HostPlacement: &machinev1beta1.HostPlacement{
347+
Affinity: ptr.To(machinev1beta1.HostAffinity("invalid")),
348+
},
349+
},
350+
},
351+
expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.hostPlacement.affinity: Invalid value: \"invalid\": affinity must be either AnyAvailable or DedicatedHost",
352+
},
353+
{
354+
name: "configure host placement dedicatedHost without dedicatedHost",
355+
platformType: osconfigv1.AWSPlatformType,
356+
clusterID: "aws-cluster",
357+
providerSpecValue: &kruntime.RawExtension{
358+
Object: &machinev1beta1.AWSMachineProviderConfig{
359+
AMI: machinev1beta1.AWSResourceReference{
360+
ID: ptr.To[string]("ami"),
361+
},
362+
InstanceType: "test",
363+
HostPlacement: &machinev1beta1.HostPlacement{
364+
Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost),
365+
},
366+
},
367+
},
368+
expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.hostPlacement.dedicatedHost: Required value: dedicatedHost is required when affinity is DedicatedHost",
369+
},
370+
{
371+
name: "configure host placement dedicatedHost with valid ID",
372+
platformType: osconfigv1.AWSPlatformType,
373+
clusterID: "aws-cluster",
374+
providerSpecValue: &kruntime.RawExtension{
375+
Object: &machinev1beta1.AWSMachineProviderConfig{
376+
AMI: machinev1beta1.AWSResourceReference{
377+
ID: ptr.To[string]("ami"),
378+
},
379+
InstanceType: "test",
380+
HostPlacement: &machinev1beta1.HostPlacement{
381+
Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost),
382+
DedicatedHost: &machinev1beta1.DedicatedHost{ID: "h-1234567890abcdef0"},
383+
},
384+
},
385+
},
386+
expectedError: "",
387+
},
388+
{
389+
name: "configure host placement AnyAvailable forbids dedicatedHost",
390+
platformType: osconfigv1.AWSPlatformType,
391+
clusterID: "aws-cluster",
392+
providerSpecValue: &kruntime.RawExtension{
393+
Object: &machinev1beta1.AWSMachineProviderConfig{
394+
AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")},
395+
InstanceType: "test",
396+
HostPlacement: &machinev1beta1.HostPlacement{
397+
Affinity: ptr.To(machinev1beta1.HostAffinityAnyAvailable),
398+
DedicatedHost: &machinev1beta1.DedicatedHost{ID: "h-09dcf61cb388b0149"},
399+
},
400+
},
401+
},
402+
expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.hostPlacement.dedicatedHost: Forbidden: dedicatedHost is required when affinity is DedicatedHost, and forbidden otherwise",
403+
},
404+
{
405+
name: "configure host placement dedicatedHost with empty ID",
406+
platformType: osconfigv1.AWSPlatformType,
407+
clusterID: "aws-cluster",
408+
providerSpecValue: &kruntime.RawExtension{
409+
Object: &machinev1beta1.AWSMachineProviderConfig{
410+
AMI: machinev1beta1.AWSResourceReference{ID: ptr.To[string]("ami")},
411+
InstanceType: "test",
412+
HostPlacement: &machinev1beta1.HostPlacement{
413+
Affinity: ptr.To(machinev1beta1.HostAffinityDedicatedHost),
414+
DedicatedHost: &machinev1beta1.DedicatedHost{ID: ""},
415+
},
416+
},
417+
},
418+
expectedError: "admission webhook \"validation.machine.machine.openshift.io\" denied the request: spec.hostPlacement.dedicatedHost.id: Invalid value: \"\": id must start with 'h-' followed by 17 lowercase hexadecimal characters (0-9 and a-f)",
419+
},
319420
{
320421
name: "with Azure and a nil provider spec value",
321422
platformType: osconfigv1.AzurePlatformType,
@@ -1958,7 +2059,7 @@ func TestMachineUpdate(t *testing.T) {
19582059
Object: object,
19592060
}
19602061
},
1961-
expectedError: `providerSpec.tagIDs: Invalid value: []string{"urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9500:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9501:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9502:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9503:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9504:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9505:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9506:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9507:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9508:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9509:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9510:GLOBAL", "urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9511:GLOBAL"}: a maximum of 10 tags are allowed`,
2062+
expectedError: `providerSpec.tagIDs: Invalid value: ["urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9500:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9501:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9502:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9503:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9504:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9505:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9506:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9507:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9508:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9509:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9510:GLOBAL","urn:vmomi:InventoryServiceTag:5736bf56-49f5-4667-b38c-b97e09dc9511:GLOBAL"]: a maximum of 10 tags are allowed`,
19622063
},
19632064
{
19642065
name: "with an VSphere ProviderSpec, removing the template",
@@ -3300,7 +3401,7 @@ func TestValidateAzureProviderSpec(t *testing.T) {
33003401
CloudName: osconfigv1.AzurePublicCloud,
33013402
},
33023403
expectedOk: false,
3303-
expectedError: "providerSpec.diagnostics.boot.customerManaged: Invalid value: v1beta1.AzureCustomerManagedBootDiagnostics{StorageAccountURI:\"https://storageaccount.blob.core.windows.net/\"}: customerManaged may not be set when type is AzureManaged",
3404+
expectedError: "providerSpec.diagnostics.boot.customerManaged: Invalid value: {\"storageAccountURI\":\"https://storageaccount.blob.core.windows.net/\"}: customerManaged may not be set when type is AzureManaged",
33043405
},
33053406
{
33063407
testCase: "with Customer Managed boot diagnostics, with a missing storage account URI",
@@ -3883,7 +3984,7 @@ func TestValidateGCPProviderSpec(t *testing.T) {
38833984
}
38843985
},
38853986
expectedOk: false,
3886-
expectedError: "providerSpec.gpus: Too many: 2: must have at most 1 items",
3987+
expectedError: "providerSpec.gpus: Too many: 2: must have at most 1 item",
38873988
},
38883989
{
38893990
testCase: "with no gpus",

pkg/webhooks/machineset_webhook_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,7 @@ func TestMachineSetUpdate(t *testing.T) {
10471047
updateMachineSet: func(ms *machinev1beta1.MachineSet) {
10481048
ms.Spec.Selector.MatchLabels["foo"] = "bar"
10491049
},
1050-
expectedError: "[spec.selector: Forbidden: selector is immutable, spec.template.metadata.labels: Invalid value: map[string]string{\"machineset-name\":\"machineset-update-abcd\"}: `selector` does not match template `labels`]",
1050+
expectedError: "[spec.selector: Forbidden: selector is immutable, spec.template.metadata.labels: Invalid value: {\"machineset-name\":\"machineset-update-abcd\"}: `selector` does not match template `labels`]",
10511051
},
10521052
{
10531053
name: "with an incompatible template labels",
@@ -1061,7 +1061,7 @@ func TestMachineSetUpdate(t *testing.T) {
10611061
"foo": "bar",
10621062
}
10631063
},
1064-
expectedError: "spec.template.metadata.labels: Invalid value: map[string]string{\"foo\":\"bar\"}: `selector` does not match template `labels`",
1064+
expectedError: "spec.template.metadata.labels: Invalid value: {\"foo\":\"bar\"}: `selector` does not match template `labels`",
10651065
},
10661066
{
10671067
name: "with a valid PowerVS ProviderSpec",

0 commit comments

Comments
 (0)