diff --git a/discovery/aws/ec2.go b/discovery/aws/ec2.go index deefa52e9c..364ec4f102 100644 --- a/discovery/aws/ec2.go +++ b/discovery/aws/ec2.go @@ -55,6 +55,7 @@ const ( ec2LabelInstanceType = ec2Label + "instance_type" ec2LabelOwnerID = ec2Label + "owner_id" ec2LabelPlatform = ec2Label + "platform" + ec2LabelDefaultIPv6Address = ec2Label + "default_ipv6_address" ec2LabelPrimaryIPv6Addresses = ec2Label + "primary_ipv6_addresses" ec2LabelPrimarySubnetID = ec2Label + "primary_subnet_id" ec2LabelPrivateDNS = ec2Label + "private_dns_name" @@ -306,7 +307,9 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error for _, r := range p.Reservations { for _, inst := range r.Instances { - if inst.PrivateIpAddress == nil { + defaultIPv6Addr, primaryIPv6Addrs, ipv6Addrs := getInstanceIPv6Addresses(&inst) + + if inst.PrivateIpAddress == nil && defaultIPv6Addr == nil { continue } @@ -319,12 +322,20 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error labels[ec2LabelOwnerID] = model.LabelValue(*r.OwnerId) } - labels[ec2LabelPrivateIP] = model.LabelValue(*inst.PrivateIpAddress) + if defaultIPv6Addr != nil { + labels[ec2LabelDefaultIPv6Address] = model.LabelValue(*defaultIPv6Addr) + } + + if inst.PrivateIpAddress != nil { + labels[ec2LabelPrivateIP] = model.LabelValue(*inst.PrivateIpAddress) + labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(*inst.PrivateIpAddress, strconv.Itoa(d.cfg.Port))) + } else { + labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(*defaultIPv6Addr, strconv.Itoa(d.cfg.Port))) + } + if inst.PrivateDnsName != nil { labels[ec2LabelPrivateDNS] = model.LabelValue(*inst.PrivateDnsName) } - addr := net.JoinHostPort(*inst.PrivateIpAddress, strconv.Itoa(d.cfg.Port)) - labels[model.AddressLabel] = model.LabelValue(addr) if inst.Platform != "" { labels[ec2LabelPlatform] = model.LabelValue(inst.Platform) @@ -334,6 +345,21 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error labels[ec2LabelPublicIP] = model.LabelValue(*inst.PublicIpAddress) labels[ec2LabelPublicDNS] = model.LabelValue(*inst.PublicDnsName) } + + if primaryIPv6Addrs != nil { + labels[ec2LabelPrimaryIPv6Addresses] = model.LabelValue( + ec2LabelSeparator + + strings.Join(primaryIPv6Addrs, ec2LabelSeparator) + + ec2LabelSeparator) + } + + if ipv6Addrs != nil { + labels[ec2LabelIPv6Addresses] = model.LabelValue( + ec2LabelSeparator + + strings.Join(ipv6Addrs, ec2LabelSeparator) + + ec2LabelSeparator) + } + labels[ec2LabelAMI] = model.LabelValue(*inst.ImageId) labels[ec2LabelAZ] = model.LabelValue(*inst.Placement.AvailabilityZone) azID, ok := d.azToAZID[*inst.Placement.AvailabilityZone] @@ -359,8 +385,6 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error labels[ec2LabelPrimarySubnetID] = model.LabelValue(*inst.SubnetId) var subnets []string - var ipv6addrs []string - var primaryipv6addrs []string subnetsMap := make(map[string]struct{}) for _, eni := range inst.NetworkInterfaces { if eni.SubnetId == nil { @@ -371,36 +395,11 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error subnetsMap[*eni.SubnetId] = struct{}{} subnets = append(subnets, *eni.SubnetId) } - - for _, ipv6addr := range eni.Ipv6Addresses { - ipv6addrs = append(ipv6addrs, *ipv6addr.Ipv6Address) - if *ipv6addr.IsPrimaryIpv6 { - // we might have to extend the slice with more than one element - // that could leave empty strings in the list which is intentional - // to keep the position/device index information - for int32(len(primaryipv6addrs)) <= *eni.Attachment.DeviceIndex { - primaryipv6addrs = append(primaryipv6addrs, "") - } - primaryipv6addrs[*eni.Attachment.DeviceIndex] = *ipv6addr.Ipv6Address - } - } } labels[ec2LabelSubnetID] = model.LabelValue( ec2LabelSeparator + strings.Join(subnets, ec2LabelSeparator) + ec2LabelSeparator) - if len(ipv6addrs) > 0 { - labels[ec2LabelIPv6Addresses] = model.LabelValue( - ec2LabelSeparator + - strings.Join(ipv6addrs, ec2LabelSeparator) + - ec2LabelSeparator) - } - if len(primaryipv6addrs) > 0 { - labels[ec2LabelPrimaryIPv6Addresses] = model.LabelValue( - ec2LabelSeparator + - strings.Join(primaryipv6addrs, ec2LabelSeparator) + - ec2LabelSeparator) - } } for _, t := range inst.Tags { @@ -417,3 +416,39 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error return []*targetgroup.Group{tg}, nil } + +func getInstanceIPv6Addresses(i *ec2Types.Instance) (*string, []string, []string) { + var primaryIPv6Addrs []string + var ipv6Addrs []string + + if i.VpcId != nil { + for _, eni := range i.NetworkInterfaces { + if eni.SubnetId == nil { + continue + } + + for _, ipv6addr := range eni.Ipv6Addresses { + ipv6Addrs = append(ipv6Addrs, *ipv6addr.Ipv6Address) + if *ipv6addr.IsPrimaryIpv6 { + // we might have to extend the slice with more than one element + // that could leave empty strings in the list which is intentional + // to keep the position/device index information + for int32(len(primaryIPv6Addrs)) <= *eni.Attachment.DeviceIndex { + primaryIPv6Addrs = append(primaryIPv6Addrs, "") + } + primaryIPv6Addrs[*eni.Attachment.DeviceIndex] = *ipv6addr.Ipv6Address + } + } + } + + // Find an IPv6 address we can use by default. Pick the first primary one if + // there is any available, if not then pick the first non-primary address. + for _, ipv6addr := range append(primaryIPv6Addrs, ipv6Addrs...) { + if ipv6addr != "" { + return &ipv6addr, primaryIPv6Addrs, ipv6Addrs + } + } + } + + return nil, primaryIPv6Addrs, ipv6Addrs +} diff --git a/discovery/aws/ec2_test.go b/discovery/aws/ec2_test.go index bd1047ffc0..b6899c1a65 100644 --- a/discovery/aws/ec2_test.go +++ b/discovery/aws/ec2_test.go @@ -108,7 +108,7 @@ func TestEC2DiscoveryRefresh(t *testing.T) { expected []*targetgroup.Group }{ { - name: "NoPrivateIp", + name: "NoPrivateIpOrIpv6", ec2Data: &ec2DataStore{ region: "region-noprivateip", azToAZID: map[string]string{ @@ -351,6 +351,7 @@ func TestEC2DiscoveryRefresh(t *testing.T) { "__meta_ec2_instance_type": model.LabelValue("instance-type-ipv6"), "__meta_ec2_ipv6_addresses": model.LabelValue(",2001:db8:2::1:1,2001:db8:2::2:1,2001:db8:2::2:2,2001:db8:2::3:1,"), "__meta_ec2_owner_id": model.LabelValue(""), + "__meta_ec2_default_ipv6_address": model.LabelValue("2001:db8:2::2:2"), "__meta_ec2_primary_ipv6_addresses": model.LabelValue(",,2001:db8:2::2:2,,2001:db8:2::3:1,"), "__meta_ec2_primary_subnet_id": model.LabelValue("azid-2"), "__meta_ec2_private_ip": model.LabelValue("9.10.11.12"), @@ -362,6 +363,69 @@ func TestEC2DiscoveryRefresh(t *testing.T) { }, }, }, + { + name: "Ipv6-Only", + ec2Data: &ec2DataStore{ + region: "region-ipv6-only", + azToAZID: map[string]string{ + "azname-a": "azid-1", + "azname-b": "azid-2", + "azname-c": "azid-3", + }, + + instances: []ec2Types.Instance{ + { + // just the minimum needed for the refresh work + ImageId: strptr("ami-ipv6-only"), + InstanceId: strptr("instance-id-ipv6-only"), + InstanceType: "instance-type-ipv6-only", + Placement: &ec2Types.Placement{AvailabilityZone: strptr("azname-b")}, + State: &ec2Types.InstanceState{Name: "running"}, + SubnetId: strptr("azid-2"), + VpcId: strptr("vpc-ipv6-only"), + // network interfaces + NetworkInterfaces: []ec2Types.InstanceNetworkInterface{ + // interface without primary IPv6, index 0 + { + Attachment: &ec2Types.InstanceNetworkInterfaceAttachment{ + DeviceIndex: aws.Int32(0), + }, + Ipv6Addresses: []ec2Types.InstanceIpv6Address{ + { + Ipv6Address: strptr("2001:db8:2::1:1"), + IsPrimaryIpv6: boolptr(false), + }, + }, + SubnetId: strptr("azid-2"), + }, + }, + }, + }, + }, + expected: []*targetgroup.Group{ + { + Source: "region-ipv6-only", + Targets: []model.LabelSet{ + { + "__address__": model.LabelValue("[2001:db8:2::1:1]:4242"), + "__meta_ec2_ami": model.LabelValue("ami-ipv6-only"), + "__meta_ec2_availability_zone": model.LabelValue("azname-b"), + "__meta_ec2_availability_zone_id": model.LabelValue("azid-2"), + "__meta_ec2_instance_id": model.LabelValue("instance-id-ipv6-only"), + "__meta_ec2_instance_state": model.LabelValue("running"), + "__meta_ec2_instance_type": model.LabelValue("instance-type-ipv6-only"), + "__meta_ec2_ipv6_addresses": model.LabelValue(",2001:db8:2::1:1,"), + "__meta_ec2_owner_id": model.LabelValue(""), + "__meta_ec2_default_ipv6_address": model.LabelValue("2001:db8:2::1:1"), + "__meta_ec2_primary_subnet_id": model.LabelValue("azid-2"), + "__meta_ec2_region": model.LabelValue("region-ipv6-only"), + "__meta_ec2_subnet_id": model.LabelValue(",azid-2,"), + "__meta_ec2_vpc_id": model.LabelValue("vpc-ipv6-only"), + }, + }, + }, + }, + }, } { t.Run(tt.name, func(t *testing.T) { client := newMockEC2Client(tt.ec2Data) diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 70a6a693c8..ee35704cf5 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -902,6 +902,7 @@ The following meta labels are available on targets during [relabeling](#relabel_ * `__meta_ec2_ipv6_addresses`: comma separated list of IPv6 addresses assigned to the instance's network interfaces, if present * `__meta_ec2_owner_id`: the ID of the AWS account that owns the EC2 instance * `__meta_ec2_platform`: the Operating System platform, set to 'windows' on Windows servers, absent otherwise +* `__meta_ec2_default_ipv6_address`: the first primary IPv6 address found if present, otherwise first non-primary IPv6 address, if present * `__meta_ec2_primary_ipv6_addresses`: comma separated list of the Primary IPv6 addresses of the instance, if present. The list is ordered based on the position of each corresponding network interface in the attachment order. * `__meta_ec2_primary_subnet_id`: the subnet ID of the primary network interface, if available * `__meta_ec2_private_dns_name`: the private DNS name of the instance, if available @@ -1832,6 +1833,7 @@ The following meta labels are available on targets during [relabeling](#relabel_ * `__meta_ec2_ipv6_addresses`: comma separated list of IPv6 addresses assigned to the instance's network interfaces, if present * `__meta_ec2_owner_id`: the ID of the AWS account that owns the EC2 instance * `__meta_ec2_platform`: the Operating System platform, set to 'windows' on Windows servers, absent otherwise +* `__meta_ec2_default_ipv6_address`: the first primary IPv6 address found if present, otherwise first non-primary IPv6 address, if present * `__meta_ec2_primary_ipv6_addresses`: comma separated list of the Primary IPv6 addresses of the instance, if present. The list is ordered based on the position of each corresponding network interface in the attachment order. * `__meta_ec2_primary_subnet_id`: the subnet ID of the primary network interface, if available * `__meta_ec2_private_dns_name`: the private DNS name of the instance, if available