diff --git a/pkg/conversion/capi2mapi/aws.go b/pkg/conversion/capi2mapi/aws.go index a476bf895..8cee1e7eb 100644 --- a/pkg/conversion/capi2mapi/aws.go +++ b/pkg/conversion/capi2mapi/aws.go @@ -167,7 +167,7 @@ func (m machineAndAWSMachineAndAWSCluster) toProviderSpec() (*mapiv1beta1.AWSMac }, // UserDataSecret - Populated below. // CredentialsSecret - Handled below. - KeyName: m.awsMachine.Spec.SSHKeyName, + KeyName: convertAWSSSHKeyNameToMAPI(m.awsMachine.Spec.SSHKeyName), // DeviceIndex - OCPCLOUD-2707: Value must always be zero. No other values are valid in MAPA even though the value is configurable. PublicIP: m.awsMachine.Spec.PublicIP, SecurityGroups: convertAWSSecurityGroupstoMAPI(m.awsMachine.Spec.AdditionalSecurityGroups), // This is the way we want to convert security groups, as the AdditionalSecurity Groups are what gets added to MAPI SGs. @@ -893,6 +893,18 @@ func ConvertAWSLoadBalancerToMAPI(loadBalancer *awsv1.AWSLoadBalancerSpec) (mapi } } +// convertAWSSSHKeyNameToMAPI normalizes CAPI's tri-state SSHKeyName for MAPI. +// CAPI: nil=cluster default, ptr("")=no key, ptr("name")=named key. +// MAPI has no tri-state: any non-nil KeyName including "" is passed to AWS RunInstances and rejected. +// ptr("") is normalized to nil so MAPI omits the field, matching the "no SSH key" intent. +func convertAWSSSHKeyNameToMAPI(sshKeyName *string) *string { + if sshKeyName != nil && *sshKeyName == "" { + return nil + } + + return sshKeyName +} + // ConvertAWSCPUOptionsToMAPI converts CAPI CPUOptions to MAPI CPUOptions. func ConvertAWSCPUOptionsToMAPI(cpuOptions awsv1.CPUOptions) *mapiv1beta1.CPUOptions { mapiCPUOptions := &mapiv1beta1.CPUOptions{} diff --git a/pkg/conversion/capi2mapi/machine_test.go b/pkg/conversion/capi2mapi/machine_test.go index 36bf2d2ca..5a5ac11a4 100644 --- a/pkg/conversion/capi2mapi/machine_test.go +++ b/pkg/conversion/capi2mapi/machine_test.go @@ -171,3 +171,12 @@ var _ = Describe("capi2mapi Machine Status Conversion", func() { }) }) }) + +var _ = DescribeTable("convertAWSSSHKeyNameToMAPI", + func(input *string, expected *string) { + Expect(convertAWSSSHKeyNameToMAPI(input)).To(Equal(expected), "input: %v", input) + }, + Entry("should return nil when input is nil", nil, nil), + Entry("should return nil when input is empty string", ptr.To(""), nil), + Entry("should return the key name when input is non-empty", ptr.To("my-key"), ptr.To("my-key")), +) diff --git a/pkg/conversion/mapi2capi/aws_fuzz_test.go b/pkg/conversion/mapi2capi/aws_fuzz_test.go index 7a554d295..81d64fbad 100644 --- a/pkg/conversion/mapi2capi/aws_fuzz_test.go +++ b/pkg/conversion/mapi2capi/aws_fuzz_test.go @@ -167,6 +167,13 @@ func (f *awsProviderFuzzer) fuzzProviderConfig(ps *mapiv1beta1.AWSMachineProvide ps.PlacementGroupPartition = nil } + // ptr("") is normalized to nil during CAPI→MAPI conversion because MAPI passes + // KeyName directly to AWS RunInstances, which rejects an empty string. + // Avoid fuzzing KeyName to ptr("") to preserve roundtrip fidelity. + if ps.KeyName != nil && *ps.KeyName == "" { + ps.KeyName = nil + } + // Copy instance-type, region and zone to the struct so they can be set at the machine labels too. f.InstanceType = ps.InstanceType f.Region = ps.Placement.Region