From eab9b696f2901cd8e446cfc230ba31dee5d5aeab Mon Sep 17 00:00:00 2001 From: Joe Adams Date: Tue, 29 Jul 2025 23:06:05 -0400 Subject: [PATCH] Upgrade AWS SDK to v2 AWS SDK v1 is end of life soon, so migrate to the V2 SDK. The credential loading should work more consistently with other projects that use the SDK and load credentials from the appropriate locations including from environment variables. This affects the EC2 and Lightsail service discovery features. Signed-off-by: Joe Adams --- discovery/aws/ec2.go | 135 +++++++++++++++++++++---------------- discovery/aws/ec2_test.go | 104 ++++++++++++++-------------- discovery/aws/lightsail.go | 92 +++++++++++++------------ go.mod | 24 +++---- go.sum | 35 +++++----- 5 files changed, 204 insertions(+), 186 deletions(-) diff --git a/discovery/aws/ec2.go b/discovery/aws/ec2.go index 7e35a1807f..cbe3f91773 100644 --- a/discovery/aws/ec2.go +++ b/discovery/aws/ec2.go @@ -23,14 +23,15 @@ import ( "strings" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/ec2metadata" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/aws/aws-sdk-go-v2/aws" + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + "github.com/aws/aws-sdk-go-v2/service/ec2" + ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/config" "github.com/prometheus/common/model" @@ -124,17 +125,28 @@ func (c *EC2SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } if c.Region == "" { - sess, err := session.NewSession() + // TODO(@sysadmind): Should we get a context from somewhere? + ctx := context.TODO() + cfg, err := awsConfig.LoadDefaultConfig(ctx) if err != nil { return err } - metadata := ec2metadata.New(sess) - region, err := metadata.Region() - if err != nil { - return errors.New("EC2 SD configuration requires a region") + + if cfg.Region != "" { + // If the region is already set in the config, use it. + // This can happen if the user has set the region in the AWS config file or environment variables. + c.Region = cfg.Region + } else { + // Attempt to get the region from the instance metadata service. + metaClient := imds.NewFromConfig(cfg) + result, err := metaClient.GetRegion(ctx, &imds.GetRegionInput{}) + if err != nil { + return errors.New("EC2 SD configuration requires a region") + } + c.Region = result.Region } - c.Region = region } + for _, f := range c.Filters { if len(f.Values) == 0 { return errors.New("EC2 SD configuration filter values cannot be empty") @@ -143,13 +155,18 @@ func (c *EC2SDConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return c.HTTPClientConfig.Validate() } +type ec2Client interface { + DescribeAvailabilityZones(ctx context.Context, params *ec2.DescribeAvailabilityZonesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeAvailabilityZonesOutput, error) + DescribeInstances(ctx context.Context, params *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) +} + // EC2Discovery periodically performs EC2-SD requests. It implements // the Discoverer interface. type EC2Discovery struct { *refresh.Discovery logger *slog.Logger cfg *EC2SDConfig - ec2 ec2iface.EC2API + ec2 ec2Client // azToAZID maps this account's availability zones to their underlying AZ // ID, e.g. eu-west-2a -> euw2-az2. Refreshes are performed sequentially, so @@ -183,46 +200,43 @@ func NewEC2Discovery(conf *EC2SDConfig, logger *slog.Logger, metrics discovery.D return d, nil } -func (d *EC2Discovery) ec2Client(context.Context) (ec2iface.EC2API, error) { +func (d *EC2Discovery) ec2Client(ctx context.Context) (ec2Client, error) { if d.ec2 != nil { return d.ec2, nil } + credProvider := credentials.NewStaticCredentialsProvider(d.cfg.AccessKey, string(d.cfg.SecretKey), "") - creds := credentials.NewStaticCredentials(d.cfg.AccessKey, string(d.cfg.SecretKey), "") - if d.cfg.AccessKey == "" && d.cfg.SecretKey == "" { - creds = nil - } - - client, err := config.NewClientFromConfig(d.cfg.HTTPClientConfig, "ec2_sd") + // Build the HTTP client from the provided HTTPClientConfig. + httpClient, err := config.NewClientFromConfig(d.cfg.HTTPClientConfig, "ec2_sd") if err != nil { return nil, err } - sess, err := session.NewSessionWithOptions(session.Options{ - Config: aws.Config{ - Endpoint: &d.cfg.Endpoint, - Region: &d.cfg.Region, - Credentials: creds, - HTTPClient: client, - }, - Profile: d.cfg.Profile, - }) + // Build the AWS config with the provided region and credentials. + cfg, err := awsConfig.LoadDefaultConfig( + ctx, + awsConfig.WithRegion(d.cfg.Region), + awsConfig.WithCredentialsProvider(credProvider), + awsConfig.WithSharedConfigProfile(d.cfg.Profile), + awsConfig.WithHTTPClient(httpClient), + ) if err != nil { - return nil, fmt.Errorf("could not create aws session: %w", err) + return nil, fmt.Errorf("could not create aws config: %w", err) } + // If the role ARN is set, assume the role to get credentials and set the credentials provider in the config. if d.cfg.RoleARN != "" { - creds := stscreds.NewCredentials(sess, d.cfg.RoleARN) - d.ec2 = ec2.New(sess, &aws.Config{Credentials: creds}) - } else { - d.ec2 = ec2.New(sess) + assumeProvider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), d.cfg.RoleARN) + cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } + d.ec2 = ec2.NewFromConfig(cfg) + return d.ec2, nil } func (d *EC2Discovery) refreshAZIDs(ctx context.Context) error { - azs, err := d.ec2.DescribeAvailabilityZonesWithContext(ctx, &ec2.DescribeAvailabilityZonesInput{}) + azs, err := d.ec2.DescribeAvailabilityZones(ctx, &ec2.DescribeAvailabilityZonesInput{}) if err != nil { return err } @@ -243,11 +257,11 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error Source: d.cfg.Region, } - var filters []*ec2.Filter + var filters []ec2Types.Filter for _, f := range d.cfg.Filters { - filters = append(filters, &ec2.Filter{ + filters = append(filters, ec2Types.Filter{ Name: aws.String(f.Name), - Values: aws.StringSlice(f.Values), + Values: f.Values, }) } @@ -262,7 +276,18 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error } input := &ec2.DescribeInstancesInput{Filters: filters} - if err := ec2Client.DescribeInstancesPagesWithContext(ctx, input, func(p *ec2.DescribeInstancesOutput, _ bool) bool { + paginator := ec2.NewDescribeInstancesPaginator(ec2Client, input) + + for paginator.HasMorePages() { + p, err := paginator.NextPage(ctx) + if err != nil { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && (awsErr.ErrorCode() == "AuthFailure" || awsErr.ErrorCode() == "UnauthorizedOperation") { + d.ec2 = nil + } + return nil, fmt.Errorf("could not describe instances: %w", err) + } + for _, r := range p.Reservations { for _, inst := range r.Instances { if inst.PrivateIpAddress == nil { @@ -285,8 +310,8 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error addr := net.JoinHostPort(*inst.PrivateIpAddress, strconv.Itoa(d.cfg.Port)) labels[model.AddressLabel] = model.LabelValue(addr) - if inst.Platform != nil { - labels[ec2LabelPlatform] = model.LabelValue(*inst.Platform) + if inst.Platform != "" { + labels[ec2LabelPlatform] = model.LabelValue(inst.Platform) } if inst.PublicIpAddress != nil { @@ -302,15 +327,15 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error "az", *inst.Placement.AvailabilityZone) } labels[ec2LabelAZID] = model.LabelValue(azID) - labels[ec2LabelInstanceState] = model.LabelValue(*inst.State.Name) - labels[ec2LabelInstanceType] = model.LabelValue(*inst.InstanceType) + labels[ec2LabelInstanceState] = model.LabelValue(inst.State.Name) + labels[ec2LabelInstanceType] = model.LabelValue(inst.InstanceType) - if inst.InstanceLifecycle != nil { - labels[ec2LabelInstanceLifecycle] = model.LabelValue(*inst.InstanceLifecycle) + if inst.InstanceLifecycle != "" { + labels[ec2LabelInstanceLifecycle] = model.LabelValue(inst.InstanceLifecycle) } - if inst.Architecture != nil { - labels[ec2LabelArch] = model.LabelValue(*inst.Architecture) + if inst.Architecture != "" { + labels[ec2LabelArch] = model.LabelValue(inst.Architecture) } if inst.VpcId != nil { @@ -337,7 +362,7 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error // 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 int64(len(primaryipv6addrs)) <= *eni.Attachment.DeviceIndex { + for int32(len(primaryipv6addrs)) <= *eni.Attachment.DeviceIndex { primaryipv6addrs = append(primaryipv6addrs, "") } primaryipv6addrs[*eni.Attachment.DeviceIndex] = *ipv6addr.Ipv6Address @@ -363,7 +388,7 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error } for _, t := range inst.Tags { - if t == nil || t.Key == nil || t.Value == nil { + if t.Key == nil || t.Value == nil { continue } name := strutil.SanitizeLabelName(*t.Key) @@ -372,13 +397,7 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error tg.Targets = append(tg.Targets, labels) } } - return true - }); err != nil { - var awsErr awserr.Error - if errors.As(err, &awsErr) && (awsErr.Code() == "AuthFailure" || awsErr.Code() == "UnauthorizedOperation") { - d.ec2 = nil - } - return nil, fmt.Errorf("could not describe instances: %w", err) } + return []*targetgroup.Group{tg}, nil } diff --git a/discovery/aws/ec2_test.go b/discovery/aws/ec2_test.go index 2955e0e02e..6845af8b89 100644 --- a/discovery/aws/ec2_test.go +++ b/discovery/aws/ec2_test.go @@ -18,10 +18,11 @@ import ( "errors" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/request" - "github.com/aws/aws-sdk-go/service/ec2" - "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + + ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types" + "github.com/prometheus/common/model" "github.com/stretchr/testify/require" "go.uber.org/goleak" @@ -39,7 +40,7 @@ func boolptr(b bool) *bool { return &b } -func int64ptr(i int64) *int64 { +func int32ptr(i int32) *int32 { return &i } @@ -51,7 +52,7 @@ type ec2DataStore struct { ownerID string - instances []*ec2.Instance + instances []ec2Types.Instance } // The tests itself. @@ -121,7 +122,7 @@ func TestEC2DiscoveryRefresh(t *testing.T) { "azname-b": "azid-2", "azname-c": "azid-3", }, - instances: []*ec2.Instance{ + instances: []ec2Types.Instance{ { InstanceId: strptr("instance-id-noprivateip"), }, @@ -143,26 +144,26 @@ func TestEC2DiscoveryRefresh(t *testing.T) { "azname-c": "azid-3", }, ownerID: "owner-id-novpc", - instances: []*ec2.Instance{ + instances: []ec2Types.Instance{ { // set every possible options and test them here - Architecture: strptr("architecture-novpc"), + Architecture: "architecture-novpc", ImageId: strptr("ami-novpc"), InstanceId: strptr("instance-id-novpc"), - InstanceLifecycle: strptr("instance-lifecycle-novpc"), - InstanceType: strptr("instance-type-novpc"), - Placement: &ec2.Placement{AvailabilityZone: strptr("azname-b")}, - Platform: strptr("platform-novpc"), + InstanceLifecycle: "instance-lifecycle-novpc", + InstanceType: "instance-type-novpc", + Placement: &ec2Types.Placement{AvailabilityZone: strptr("azname-b")}, + Platform: "platform-novpc", PrivateDnsName: strptr("private-dns-novpc"), PrivateIpAddress: strptr("1.2.3.4"), PublicDnsName: strptr("public-dns-novpc"), PublicIpAddress: strptr("42.42.42.2"), - State: &ec2.InstanceState{Name: strptr("running")}, + State: &ec2Types.InstanceState{Name: "running"}, // test tags once and for all - Tags: []*ec2.Tag{ + Tags: []ec2Types.Tag{ {Key: strptr("tag-1-key"), Value: strptr("tag-1-value")}, {Key: strptr("tag-2-key"), Value: strptr("tag-2-value")}, - nil, + {}, {Value: strptr("tag-4-value")}, {Key: strptr("tag-5-key")}, }, @@ -206,22 +207,22 @@ func TestEC2DiscoveryRefresh(t *testing.T) { "azname-b": "azid-2", "azname-c": "azid-3", }, - instances: []*ec2.Instance{ + instances: []ec2Types.Instance{ { // just the minimum needed for the refresh work ImageId: strptr("ami-ipv4"), InstanceId: strptr("instance-id-ipv4"), - InstanceType: strptr("instance-type-ipv4"), - Placement: &ec2.Placement{AvailabilityZone: strptr("azname-c")}, + InstanceType: "instance-type-ipv4", + Placement: &ec2Types.Placement{AvailabilityZone: strptr("azname-c")}, PrivateIpAddress: strptr("5.6.7.8"), - State: &ec2.InstanceState{Name: strptr("running")}, + State: &ec2Types.InstanceState{Name: "running"}, SubnetId: strptr("azid-3"), VpcId: strptr("vpc-ipv4"), // network interfaces - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []ec2Types.InstanceNetworkInterface{ // interface without subnet -> should be ignored { - Ipv6Addresses: []*ec2.InstanceIpv6Address{ + Ipv6Addresses: []ec2Types.InstanceIpv6Address{ { Ipv6Address: strptr("2001:db8:1::1"), IsPrimaryIpv6: boolptr(true), @@ -230,12 +231,12 @@ func TestEC2DiscoveryRefresh(t *testing.T) { }, // interface with subnet, no IPv6 { - Ipv6Addresses: []*ec2.InstanceIpv6Address{}, + Ipv6Addresses: []ec2Types.InstanceIpv6Address{}, SubnetId: strptr("azid-3"), }, // interface with another subnet, no IPv6 { - Ipv6Addresses: []*ec2.InstanceIpv6Address{}, + Ipv6Addresses: []ec2Types.InstanceIpv6Address{}, SubnetId: strptr("azid-1"), }, }, @@ -274,25 +275,25 @@ func TestEC2DiscoveryRefresh(t *testing.T) { "azname-b": "azid-2", "azname-c": "azid-3", }, - instances: []*ec2.Instance{ + instances: []ec2Types.Instance{ { // just the minimum needed for the refresh work ImageId: strptr("ami-ipv6"), InstanceId: strptr("instance-id-ipv6"), - InstanceType: strptr("instance-type-ipv6"), - Placement: &ec2.Placement{AvailabilityZone: strptr("azname-b")}, + InstanceType: "instance-type-ipv6", + Placement: &ec2Types.Placement{AvailabilityZone: strptr("azname-b")}, PrivateIpAddress: strptr("9.10.11.12"), - State: &ec2.InstanceState{Name: strptr("running")}, + State: &ec2Types.InstanceState{Name: "running"}, SubnetId: strptr("azid-2"), VpcId: strptr("vpc-ipv6"), // network interfaces - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []ec2Types.InstanceNetworkInterface{ // interface without primary IPv6, index 2 { - Attachment: &ec2.InstanceNetworkInterfaceAttachment{ - DeviceIndex: int64ptr(3), + Attachment: &ec2Types.InstanceNetworkInterfaceAttachment{ + DeviceIndex: int32ptr(3), }, - Ipv6Addresses: []*ec2.InstanceIpv6Address{ + Ipv6Addresses: []ec2Types.InstanceIpv6Address{ { Ipv6Address: strptr("2001:db8:2::1:1"), IsPrimaryIpv6: boolptr(false), @@ -302,10 +303,10 @@ func TestEC2DiscoveryRefresh(t *testing.T) { }, // interface with primary IPv6, index 1 { - Attachment: &ec2.InstanceNetworkInterfaceAttachment{ - DeviceIndex: int64ptr(1), + Attachment: &ec2Types.InstanceNetworkInterfaceAttachment{ + DeviceIndex: int32ptr(1), }, - Ipv6Addresses: []*ec2.InstanceIpv6Address{ + Ipv6Addresses: []ec2Types.InstanceIpv6Address{ { Ipv6Address: strptr("2001:db8:2::2:1"), IsPrimaryIpv6: boolptr(false), @@ -319,10 +320,10 @@ func TestEC2DiscoveryRefresh(t *testing.T) { }, // interface with primary IPv6, index 3 { - Attachment: &ec2.InstanceNetworkInterfaceAttachment{ - DeviceIndex: int64ptr(3), + Attachment: &ec2Types.InstanceNetworkInterfaceAttachment{ + DeviceIndex: int32ptr(3), }, - Ipv6Addresses: []*ec2.InstanceIpv6Address{ + Ipv6Addresses: []ec2Types.InstanceIpv6Address{ { Ipv6Address: strptr("2001:db8:2::3:1"), IsPrimaryIpv6: boolptr(true), @@ -332,10 +333,10 @@ func TestEC2DiscoveryRefresh(t *testing.T) { }, // interface without primary IPv6, index 0 { - Attachment: &ec2.InstanceNetworkInterfaceAttachment{ - DeviceIndex: int64ptr(0), + Attachment: &ec2Types.InstanceNetworkInterfaceAttachment{ + DeviceIndex: int32ptr(0), }, - Ipv6Addresses: []*ec2.InstanceIpv6Address{}, + Ipv6Addresses: []ec2Types.InstanceIpv6Address{}, SubnetId: strptr("azid-3"), }, }, @@ -388,7 +389,6 @@ func TestEC2DiscoveryRefresh(t *testing.T) { // EC2 client mock. type mockEC2Client struct { - ec2iface.EC2API ec2Data ec2DataStore } @@ -399,16 +399,16 @@ func newMockEC2Client(ec2Data *ec2DataStore) *mockEC2Client { return &client } -func (m *mockEC2Client) DescribeAvailabilityZonesWithContext(_ aws.Context, _ *ec2.DescribeAvailabilityZonesInput, _ ...request.Option) (*ec2.DescribeAvailabilityZonesOutput, error) { +func (m *mockEC2Client) DescribeAvailabilityZones(_ context.Context, _ *ec2.DescribeAvailabilityZonesInput, _ ...func(*ec2.Options)) (*ec2.DescribeAvailabilityZonesOutput, error) { if len(m.ec2Data.azToAZID) == 0 { return nil, errors.New("No AZs found") } - azs := make([]*ec2.AvailabilityZone, len(m.ec2Data.azToAZID)) + azs := make([]ec2Types.AvailabilityZone, len(m.ec2Data.azToAZID)) i := 0 for k, v := range m.ec2Data.azToAZID { - azs[i] = &ec2.AvailabilityZone{ + azs[i] = ec2Types.AvailabilityZone{ ZoneName: strptr(k), ZoneId: strptr(v), } @@ -420,15 +420,13 @@ func (m *mockEC2Client) DescribeAvailabilityZonesWithContext(_ aws.Context, _ *e }, nil } -func (m *mockEC2Client) DescribeInstancesPagesWithContext(_ aws.Context, _ *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool, _ ...request.Option) error { - r := ec2.Reservation{} - r.SetInstances(m.ec2Data.instances) - r.SetOwnerId(m.ec2Data.ownerID) +func (m *mockEC2Client) DescribeInstances(_ context.Context, _ *ec2.DescribeInstancesInput, _ ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { + r := ec2Types.Reservation{} + r.Instances = append(r.Instances, m.ec2Data.instances...) + r.OwnerId = aws.String(m.ec2Data.ownerID) o := ec2.DescribeInstancesOutput{} - o.SetReservations([]*ec2.Reservation{&r}) + o.Reservations = []ec2Types.Reservation{r} - _ = fn(&o, true) - - return nil + return &o, nil } diff --git a/discovery/aws/lightsail.go b/discovery/aws/lightsail.go index ff1059ede0..86ed73837a 100644 --- a/discovery/aws/lightsail.go +++ b/discovery/aws/lightsail.go @@ -23,13 +23,14 @@ import ( "strings" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" - "github.com/aws/aws-sdk-go/aws/ec2metadata" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/lightsail" + "github.com/aws/aws-sdk-go-v2/aws" + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + "github.com/aws/aws-sdk-go-v2/service/lightsail" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/aws/smithy-go" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/config" "github.com/prometheus/common/model" @@ -106,19 +107,26 @@ func (c *LightsailSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) err return err } if c.Region == "" { - sess, err := session.NewSession() + // TODO(@sysadmind): Should we get a context from somewhere? + ctx := context.TODO() + cfg, err := awsConfig.LoadDefaultConfig(ctx) if err != nil { return err } - metadata := ec2metadata.New(sess) - - region, err := metadata.Region() - if err != nil { - //nolint:staticcheck // Capitalized first word. - return errors.New("Lightsail SD configuration requires a region") + if cfg.Region != "" { + // If the region is already set in the config, use it. + // This can happen if the user has set the region in the AWS config file or environment variables. + c.Region = cfg.Region + } else { + // Attempt to get the region from the instance metadata service. + metaClient := imds.NewFromConfig(cfg) + result, err := metaClient.GetRegion(ctx, &imds.GetRegionInput{}) + if err != nil { + return errors.New("EC2 SD configuration requires a region") + } + c.Region = result.Region } - c.Region = region } return c.HTTPClientConfig.Validate() } @@ -128,7 +136,7 @@ func (c *LightsailSDConfig) UnmarshalYAML(unmarshal func(interface{}) error) err type LightsailDiscovery struct { *refresh.Discovery cfg *LightsailSDConfig - lightsail *lightsail.Lightsail + lightsail *lightsail.Client } // NewLightsailDiscovery returns a new LightsailDiscovery which periodically refreshes its targets. @@ -157,46 +165,44 @@ func NewLightsailDiscovery(conf *LightsailSDConfig, logger *slog.Logger, metrics return d, nil } -func (d *LightsailDiscovery) lightsailClient() (*lightsail.Lightsail, error) { +func (d *LightsailDiscovery) lightsailClient(ctx context.Context) (*lightsail.Client, error) { if d.lightsail != nil { return d.lightsail, nil } - creds := credentials.NewStaticCredentials(d.cfg.AccessKey, string(d.cfg.SecretKey), "") - if d.cfg.AccessKey == "" && d.cfg.SecretKey == "" { - creds = nil - } + credProvider := credentials.NewStaticCredentialsProvider(d.cfg.AccessKey, string(d.cfg.SecretKey), "") - client, err := config.NewClientFromConfig(d.cfg.HTTPClientConfig, "lightsail_sd") + // Build the HTTP client from the provided HTTPClientConfig. + httpClient, err := config.NewClientFromConfig(d.cfg.HTTPClientConfig, "lightsail_sd") if err != nil { return nil, err } - sess, err := session.NewSessionWithOptions(session.Options{ - Config: aws.Config{ - Endpoint: &d.cfg.Endpoint, - Region: &d.cfg.Region, - Credentials: creds, - HTTPClient: client, - }, - Profile: d.cfg.Profile, - }) + // Build the AWS config with the provided region and credentials. + cfg, err := awsConfig.LoadDefaultConfig( + ctx, + awsConfig.WithRegion(d.cfg.Region), + awsConfig.WithCredentialsProvider(credProvider), + awsConfig.WithSharedConfigProfile(d.cfg.Profile), + awsConfig.WithHTTPClient(httpClient), + ) if err != nil { - return nil, fmt.Errorf("could not create aws session: %w", err) + return nil, fmt.Errorf("could not create aws config: %w", err) } + // If the role ARN is set, assume the role to get credentials and set the credentials provider in the config. if d.cfg.RoleARN != "" { - creds := stscreds.NewCredentials(sess, d.cfg.RoleARN) - d.lightsail = lightsail.New(sess, &aws.Config{Credentials: creds}) - } else { - d.lightsail = lightsail.New(sess) + assumeProvider := stscreds.NewAssumeRoleProvider(sts.NewFromConfig(cfg), d.cfg.RoleARN) + cfg.Credentials = aws.NewCredentialsCache(assumeProvider) } + d.lightsail = lightsail.NewFromConfig(cfg) + return d.lightsail, nil } func (d *LightsailDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { - lightsailClient, err := d.lightsailClient() + lightsailClient, err := d.lightsailClient(ctx) if err != nil { return nil, err } @@ -207,10 +213,10 @@ func (d *LightsailDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, input := &lightsail.GetInstancesInput{} - output, err := lightsailClient.GetInstancesWithContext(ctx, input) + output, err := lightsailClient.GetInstances(ctx, input) if err != nil { - var awsErr awserr.Error - if errors.As(err, &awsErr) && (awsErr.Code() == "AuthFailure" || awsErr.Code() == "UnauthorizedOperation") { + var awsErr smithy.APIError + if errors.As(err, &awsErr) && (awsErr.ErrorCode() == "AuthFailure" || awsErr.ErrorCode() == "UnauthorizedOperation") { d.lightsail = nil } return nil, fmt.Errorf("could not get instances: %w", err) @@ -241,9 +247,7 @@ func (d *LightsailDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, if len(inst.Ipv6Addresses) > 0 { var ipv6addrs []string - for _, ipv6addr := range inst.Ipv6Addresses { - ipv6addrs = append(ipv6addrs, *ipv6addr) - } + ipv6addrs = append(ipv6addrs, inst.Ipv6Addresses...) labels[lightsailLabelIPv6Addresses] = model.LabelValue( lightsailLabelSeparator + strings.Join(ipv6addrs, lightsailLabelSeparator) + @@ -251,7 +255,7 @@ func (d *LightsailDiscovery) refresh(ctx context.Context) ([]*targetgroup.Group, } for _, t := range inst.Tags { - if t == nil || t.Key == nil || t.Value == nil { + if t.Key == nil || t.Value == nil { continue } name := strutil.SanitizeLabelName(*t.Key) diff --git a/go.mod b/go.mod index f038ecf33e..e6ddf38e02 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,14 @@ require ( github.com/KimMachineGun/automemlimit v0.7.3 github.com/alecthomas/kingpin/v2 v2.4.0 github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b - github.com/aws/aws-sdk-go v1.55.7 + github.com/aws/aws-sdk-go-v2 v1.37.0 + github.com/aws/aws-sdk-go-v2/config v1.29.14 + github.com/aws/aws-sdk-go-v2/credentials v1.17.67 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.237.0 + github.com/aws/aws-sdk-go-v2/service/lightsail v1.44.0 + github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 + github.com/aws/smithy-go v1.22.5 github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 github.com/cespare/xxhash/v2 v2.3.0 github.com/dennwc/varint v1.0.0 @@ -95,19 +102,13 @@ require ( ) require ( - github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.29.14 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect - github.com/aws/smithy-go v1.22.2 // indirect github.com/cenkalti/backoff/v5 v5.0.2 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect @@ -174,7 +175,6 @@ require ( github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/golang-lru v0.6.0 // indirect github.com/hashicorp/serf v0.10.1 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect diff --git a/go.sum b/go.sum index 4db727944b..c3b666270d 100644 --- a/go.sum +++ b/go.sum @@ -49,34 +49,36 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= -github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= -github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2 v1.37.0 h1:YtCOESR/pN4j5oA7cVHSfOwIcuh/KwHC4DOSXFbv5F0= +github.com/aws/aws-sdk-go-v2 v1.37.0/go.mod h1:9Q0OoGQoboYIAJyslFyF1f5K1Ryddop8gqMhWx/n4Wg= github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0 h1:H2iZoqW/v2Jnrh1FnU725Bq6KJ0k2uP63yH+DcY+HUI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.0/go.mod h1:L0FqLbwMXHvNC/7crWV1iIxUlOKYZUE8KuTIA+TozAI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0 h1:EDped/rNzAhFPhVY0sDGbtD16OKqksfA8OjF/kLEgw8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.0/go.mod h1:uUI335jvzpZRPpjYx6ODc/wg1qH+NnoSTK/FwVeK0C0= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.237.0 h1:XHE2G+yaDQql32FZt19QmQt4WuisqQJIkMUSCxeCUl8= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.237.0/go.mod h1:t11/j/nH9i6bbsPH9xc04BJOsV2nVPUqrB67/TLDsyM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0 h1:6+lZi2JeGKtCraAj1rpoZfKqnQ9SptseRZioejfUOLM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.0/go.mod h1:eb3gfbVIxIoGgJsi9pGne19dhCBpK6opTYpQqAmdy44= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0 h1:eRhU3Sh8dGbaniI6B+I48XJMrTPRkK4DKo+vqIxziOU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.0/go.mod h1:paNLV18DZ6FnWE/bd06RIKPDIFpjuvCkGKWTG/GDBeM= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.44.0 h1:QiiCqpKy0prxq+92uWfESzcb7/8Y9JAamcMOzVYLEoM= +github.com/aws/aws-sdk-go-v2/service/lightsail v1.44.0/go.mod h1:ESppxYqXQCpCY+KWl3BdkQjmsQX6zxKP39SnDtRDoU0= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= -github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= -github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= +github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -289,10 +291,6 @@ github.com/ionos-cloud/sdk-go/v6 v6.3.4 h1:jTvGl4LOF8v8OYoEIBNVwbFoqSGAFqn6vGE7s github.com/ionos-cloud/sdk-go/v6 v6.3.4/go.mod h1:wCVwNJ/21W29FWFUv+fNawOTMlFoP1dS3L+ZuztFW48= github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= github.com/jarcoal/httpmock v1.4.0/go.mod h1:ftW1xULwo+j0R0JJkJIIi7UKigZUXCLLanykgjwBXL0= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -708,7 +706,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=