diff --git a/api/v1beta1/awscluster_conversion.go b/api/v1beta1/awscluster_conversion.go index e6d9982cbd..9e1f4d3451 100644 --- a/api/v1beta1/awscluster_conversion.go +++ b/api/v1beta1/awscluster_conversion.go @@ -59,6 +59,7 @@ func (src *AWSCluster) ConvertTo(dstRaw conversion.Hub) error { dst.Status.Bastion.PlacementGroupPartition = restored.Status.Bastion.PlacementGroupPartition dst.Status.Bastion.PrivateDNSName = restored.Status.Bastion.PrivateDNSName dst.Status.Bastion.PublicIPOnLaunch = restored.Status.Bastion.PublicIPOnLaunch + dst.Status.Bastion.NetworkInterfaceType = restored.Status.Bastion.NetworkInterfaceType dst.Status.Bastion.CapacityReservationID = restored.Status.Bastion.CapacityReservationID } dst.Spec.Partition = restored.Spec.Partition diff --git a/api/v1beta1/awsmachine_conversion.go b/api/v1beta1/awsmachine_conversion.go index 6044416cdf..8a9323f317 100644 --- a/api/v1beta1/awsmachine_conversion.go +++ b/api/v1beta1/awsmachine_conversion.go @@ -42,6 +42,7 @@ func (src *AWSMachine) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.PrivateDNSName = restored.Spec.PrivateDNSName dst.Spec.SecurityGroupOverrides = restored.Spec.SecurityGroupOverrides dst.Spec.CapacityReservationID = restored.Spec.CapacityReservationID + dst.Spec.NetworkInterfaceType = restored.Spec.NetworkInterfaceType if restored.Spec.ElasticIPPool != nil { if dst.Spec.ElasticIPPool == nil { dst.Spec.ElasticIPPool = &infrav1.ElasticIPPool{} @@ -104,6 +105,7 @@ func (r *AWSMachineTemplate) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Template.Spec.PrivateDNSName = restored.Spec.Template.Spec.PrivateDNSName dst.Spec.Template.Spec.SecurityGroupOverrides = restored.Spec.Template.Spec.SecurityGroupOverrides dst.Spec.Template.Spec.CapacityReservationID = restored.Spec.Template.Spec.CapacityReservationID + dst.Spec.Template.Spec.NetworkInterfaceType = restored.Spec.Template.Spec.NetworkInterfaceType if restored.Spec.Template.Spec.ElasticIPPool != nil { if dst.Spec.Template.Spec.ElasticIPPool == nil { dst.Spec.Template.Spec.ElasticIPPool = &infrav1.ElasticIPPool{} diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index 2d1641f424..1799bc24ce 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -1415,6 +1415,7 @@ func autoConvert_v1beta2_AWSMachineSpec_To_v1beta1_AWSMachineSpec(in *v1beta2.AW out.RootVolume = (*Volume)(unsafe.Pointer(in.RootVolume)) out.NonRootVolumes = *(*[]Volume)(unsafe.Pointer(&in.NonRootVolumes)) out.NetworkInterfaces = *(*[]string)(unsafe.Pointer(&in.NetworkInterfaces)) + // WARNING: in.NetworkInterfaceType requires manual conversion: does not exist in peer-type out.UncompressedUserData = (*bool)(unsafe.Pointer(in.UncompressedUserData)) if err := Convert_v1beta2_CloudInit_To_v1beta1_CloudInit(&in.CloudInit, &out.CloudInit, s); err != nil { return err @@ -2025,6 +2026,7 @@ func autoConvert_v1beta2_Instance_To_v1beta1_Instance(in *v1beta2.Instance, out out.RootVolume = (*Volume)(unsafe.Pointer(in.RootVolume)) out.NonRootVolumes = *(*[]Volume)(unsafe.Pointer(&in.NonRootVolumes)) out.NetworkInterfaces = *(*[]string)(unsafe.Pointer(&in.NetworkInterfaces)) + // WARNING: in.NetworkInterfaceType requires manual conversion: does not exist in peer-type out.Tags = *(*map[string]string)(unsafe.Pointer(&in.Tags)) out.AvailabilityZone = in.AvailabilityZone out.SpotMarketOptions = (*SpotMarketOptions)(unsafe.Pointer(in.SpotMarketOptions)) diff --git a/api/v1beta2/awsmachine_types.go b/api/v1beta2/awsmachine_types.go index 39a649a0e5..9e68e78a11 100644 --- a/api/v1beta2/awsmachine_types.go +++ b/api/v1beta2/awsmachine_types.go @@ -54,6 +54,16 @@ const ( IgnitionStorageTypeOptionUnencryptedUserData = IgnitionStorageTypeOption("UnencryptedUserData") ) +// NetworkInterfaceType is the type of network interface. +type NetworkInterfaceType string + +const ( + // NetworkInterfaceTypeENI means the network interface type is Elastic Network Interface. + NetworkInterfaceTypeENI NetworkInterfaceType = NetworkInterfaceType("Interface") + // NetworkInterfaceTypeEFAWithENAInterface means the network interface type is Elastic Fabric Adapter with Elastic Network Adapter. + NetworkInterfaceTypeEFAWithENAInterface NetworkInterfaceType = NetworkInterfaceType("EFA") +) + // AWSMachineSpec defines the desired state of an Amazon EC2 instance. type AWSMachineSpec struct { // ProviderID is the unique identifier as specified by the cloud provider. @@ -153,6 +163,12 @@ type AWSMachineSpec struct { // +kubebuilder:validation:MaxItems=2 NetworkInterfaces []string `json:"networkInterfaces,omitempty"` + // NetworkInterfaceType is the interface type of the primary network Interface. + // If not specified, AWS applies a default value. + // +kubebuilder:validation:Enum=interface;efa + // +optional + NetworkInterfaceType NetworkInterfaceType `json:"networkInterfaceType,omitempty"` + // UncompressedUserData specify whether the user data is gzip-compressed before it is sent to ec2 instance. // cloud-init has built-in support for gzip-compressed user data // user data stored in aws secret manager is always gzip-compressed. diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index 978d5310f2..74ce694e3e 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -217,6 +217,9 @@ type Instance struct { // Specifies ENIs attached to instance NetworkInterfaces []string `json:"networkInterfaces,omitempty"` + // NetworkInterfaceType is the interface type of the primary network Interface. + NetworkInterfaceType NetworkInterfaceType `json:"networkInterfaceType,omitempty"` + // The tags associated with the instance. Tags map[string]string `json:"tags,omitempty"` diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index 21be0fc920..e211b09e29 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -1210,6 +1210,10 @@ spec: instanceState: description: The current state of the instance. type: string + networkInterfaceType: + description: NetworkInterfaceType is the interface type of the + primary network Interface. + type: string networkInterfaces: description: Specifies ENIs attached to instance items: @@ -3250,6 +3254,10 @@ spec: instanceState: description: The current state of the instance. type: string + networkInterfaceType: + description: NetworkInterfaceType is the interface type of the + primary network Interface. + type: string networkInterfaces: description: Specifies ENIs attached to instance items: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml index 3ccb164ec1..c186684c56 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsclusters.yaml @@ -2177,6 +2177,10 @@ spec: instanceState: description: The current state of the instance. type: string + networkInterfaceType: + description: NetworkInterfaceType is the interface type of the + primary network Interface. + type: string networkInterfaces: description: Specifies ENIs attached to instance items: diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml index 70e7728369..8b7ea86c0a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachines.yaml @@ -878,6 +878,13 @@ spec: m4.xlarge' minLength: 2 type: string + networkInterfaceType: + description: NetworkInterfaceType is the interface type of the primary + network Interface. + enum: + - interface + - efa + type: string networkInterfaces: description: |- NetworkInterfaces is a list of ENIs to associate with the instance. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml index a679a68bc8..b8e1737c95 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmachinetemplates.yaml @@ -812,6 +812,13 @@ spec: Example: m4.xlarge' minLength: 2 type: string + networkInterfaceType: + description: NetworkInterfaceType is the interface type of + the primary network Interface. + enum: + - interface + - efa + type: string networkInterfaces: description: |- NetworkInterfaces is a list of ENIs to associate with the instance. diff --git a/pkg/cloud/services/ec2/instances.go b/pkg/cloud/services/ec2/instances.go index 1da1893ff7..bcfa80ea8e 100644 --- a/pkg/cloud/services/ec2/instances.go +++ b/pkg/cloud/services/ec2/instances.go @@ -113,11 +113,12 @@ func (s *Service) CreateInstance(scope *scope.MachineScope, userData []byte, use s.scope.Debug("Creating an instance for a machine") input := &infrav1.Instance{ - Type: scope.AWSMachine.Spec.InstanceType, - IAMProfile: scope.AWSMachine.Spec.IAMInstanceProfile, - RootVolume: scope.AWSMachine.Spec.RootVolume.DeepCopy(), - NonRootVolumes: scope.AWSMachine.Spec.NonRootVolumes, - NetworkInterfaces: scope.AWSMachine.Spec.NetworkInterfaces, + Type: scope.AWSMachine.Spec.InstanceType, + IAMProfile: scope.AWSMachine.Spec.IAMInstanceProfile, + RootVolume: scope.AWSMachine.Spec.RootVolume.DeepCopy(), + NonRootVolumes: scope.AWSMachine.Spec.NonRootVolumes, + NetworkInterfaces: scope.AWSMachine.Spec.NetworkInterfaces, + NetworkInterfaceType: scope.AWSMachine.Spec.NetworkInterfaceType, } // Make sure to use the MachineScope here to get the merger of AWSCluster and AWSMachine tags @@ -582,6 +583,10 @@ func (s *Service) runInstance(role string, i *infrav1.Instance) (*infrav1.Instan } } + if i.NetworkInterfaceType != "" { + input.NetworkInterfaces[0].InterfaceType = aws.String(string(i.NetworkInterfaceType)) + } + if i.IAMProfile != "" { input.IamInstanceProfile = &ec2.IamInstanceProfileSpecification{ Name: aws.String(i.IAMProfile), diff --git a/pkg/cloud/services/ec2/instances_test.go b/pkg/cloud/services/ec2/instances_test.go index d8e48602ee..c41cce6d80 100644 --- a/pkg/cloud/services/ec2/instances_test.go +++ b/pkg/cloud/services/ec2/instances_test.go @@ -2450,6 +2450,134 @@ func TestCreateInstance(t *testing.T) { } }, }, + { + name: "efa interface type", + machine: &clusterv1.Machine{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"set": "node"}, + }, + Spec: clusterv1.MachineSpec{ + Bootstrap: clusterv1.Bootstrap{ + DataSecretName: ptr.To[string]("bootstrap-data"), + }, + }, + }, + machineConfig: &infrav1.AWSMachineSpec{ + AMI: infrav1.AMIReference{ + ID: aws.String("abc"), + }, + InstanceType: "m5.large", + NetworkInterfaceType: "EFA", + }, + awsCluster: &infrav1.AWSCluster{ + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: infrav1.AWSClusterSpec{ + NetworkSpec: infrav1.NetworkSpec{ + Subnets: infrav1.Subnets{ + infrav1.SubnetSpec{ + ID: "subnet-1", + IsPublic: false, + }, + infrav1.SubnetSpec{ + IsPublic: false, + }, + }, + VPC: infrav1.VPCSpec{ + ID: "vpc-test", + }, + }, + }, + Status: infrav1.AWSClusterStatus{ + Network: infrav1.NetworkStatus{ + SecurityGroups: map[infrav1.SecurityGroupRole]infrav1.SecurityGroup{ + infrav1.SecurityGroupControlPlane: { + ID: "1", + }, + infrav1.SecurityGroupNode: { + ID: "2", + }, + infrav1.SecurityGroupLB: { + ID: "3", + }, + }, + APIServerELB: infrav1.LoadBalancer{ + DNSName: "test-apiserver.us-east-1.aws", + }, + }, + }, + }, + expect: func(m *mocks.MockEC2APIMockRecorder) { + m. + DescribeInstanceTypesWithContext(context.TODO(), gomock.Eq(&ec2.DescribeInstanceTypesInput{ + InstanceTypes: []*string{ + aws.String("m5.large"), + }, + })). + Return(&ec2.DescribeInstanceTypesOutput{ + InstanceTypes: []*ec2.InstanceTypeInfo{ + { + ProcessorInfo: &ec2.ProcessorInfo{ + SupportedArchitectures: []*string{ + aws.String("x86_64"), + }, + }, + }, + }, + }, nil) + m. + RunInstancesWithContext(context.TODO(), gomock.Any()). + Do(func(_ context.Context, in *ec2.RunInstancesInput, _ ...request.Option) { + if len(in.NetworkInterfaces) == 0 { + t.Fatalf("expected a NetworkInterface to be defined") + } + if in.NetworkInterfaces[0].Groups == nil { + t.Fatalf("expected security groups to be set") + } + if interfaceType := aws.StringValue(in.NetworkInterfaces[0].InterfaceType); interfaceType != "EFA" { + t.Fatalf("expected interface type to be \"EFA\": got %q", interfaceType) + } + }). + Return(&ec2.Reservation{ + Instances: []*ec2.Instance{ + { + State: &ec2.InstanceState{ + Name: aws.String(ec2.InstanceStateNamePending), + }, + IamInstanceProfile: &ec2.IamInstanceProfile{ + Arn: aws.String("arn:aws:iam::123456789012:instance-profile/foo"), + }, + InstanceId: aws.String("two"), + InstanceType: aws.String("m5.large"), + SubnetId: aws.String("subnet-1"), + ImageId: aws.String("ami-1"), + RootDeviceName: aws.String("device-1"), + BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{ + { + DeviceName: aws.String("device-1"), + Ebs: &ec2.EbsInstanceBlockDevice{ + VolumeId: aws.String("volume-1"), + }, + }, + }, + Placement: &ec2.Placement{ + AvailabilityZone: &az, + }, + }, + }, + }, nil) + m. + DescribeNetworkInterfacesWithContext(context.TODO(), gomock.Any()). + Return(&ec2.DescribeNetworkInterfacesOutput{ + NetworkInterfaces: []*ec2.NetworkInterface{}, + NextToken: nil, + }, nil) + }, + check: func(instance *infrav1.Instance, err error) { + if err != nil { + t.Fatalf("did not expect error: %v", err) + } + }, + }, { name: "public IP true and private subnet ID given", machine: &clusterv1.Machine{