From 334732967d4108be7051804df26c3eb1d61df8ba Mon Sep 17 00:00:00 2001 From: Damian Debkowski Date: Tue, 4 Jul 2023 11:03:24 -0700 Subject: [PATCH] chore: host plugin import aws-sdk-go-v2 (#27) --- go.mod | 11 +- go.sum | 14 +- plugin/service/host/plugin.go | 145 ++++++++----------- plugin/service/host/plugin_test.go | 202 +++++++++++++++------------ plugin/service/host/state.go | 90 +++++++++--- plugin/service/host/state_test.go | 3 +- plugin/service/host/testing.go | 27 ++-- plugin/service/storage/state_test.go | 2 +- 8 files changed, 283 insertions(+), 211 deletions(-) diff --git a/go.mod b/go.mod index 62dfd23..7e72051 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.20 require ( github.com/aws/aws-sdk-go v1.44.80 - github.com/aws/aws-sdk-go-v2 v1.17.7 + github.com/aws/aws-sdk-go-v2 v1.18.0 github.com/aws/aws-sdk-go-v2/config v1.18.19 github.com/aws/aws-sdk-go-v2/credentials v1.13.18 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 + github.com/aws/smithy-go v1.13.5 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.0 github.com/hashicorp/boundary/sdk v0.0.33 @@ -22,18 +24,17 @@ require ( require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 // indirect - github.com/aws/smithy-go v1.13.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.14.1 // indirect github.com/golang/protobuf v1.5.2 // indirect diff --git a/go.sum b/go.sum index 19adea4..b1604f3 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,9 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.44.80 h1:jEXGecSgPdvM5KnyDsSgFhZSm7WwaTp4h544Im4SfhI= github.com/aws/aws-sdk-go v1.44.80/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg= github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY= +github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw= @@ -17,20 +18,25 @@ github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1 github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23 h1:DWYZIsyqagnWL00f8M/SOr9fN063OEQWn9LLTbdYXsk= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.23/go.mod h1:uIiFgURZbACBEQJfqTZPb/jxO7R+9LeoHUFudtIdeQI= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0 h1:NXi4pNJWjAaiI56P1Rl8DC9A4jMNRE00WNBsDua5WRg= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.99.0/go.mod h1:L3ZT0N/vBsw77mOAawXmRnREpEjcHd2v5Hzf7AkIH8M= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26 h1:CeuSeq/8FnYpPtnuIeLQEEvDv9zUjneuYi8EghMBdwQ= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.26/go.mod h1:2UqAAwMUXKeRkAHIlDJqvMVgOWkUi/AUXPk/YIe+Dg4= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0 h1:e2ooMhpYGhDnBfSvIyusvAwX7KexuZaHbQY2Dyei7VU= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.0/go.mod h1:bh2E0CXKZsQN+faiKVqC40vfNMAWheoULBCnEgO9K+8= github.com/aws/aws-sdk-go-v2/service/s3 v1.31.1 h1:PJH4I+qYjPXclKRbVCW47iYUvtXEh1u6YmDhn5J8VQE= diff --git a/plugin/service/host/plugin.go b/plugin/service/host/plugin.go index 97692a6..4c725c1 100644 --- a/plugin/service/host/plugin.go +++ b/plugin/service/host/plugin.go @@ -9,9 +9,10 @@ import ( "fmt" "strings" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" + awserr "github.com/aws/smithy-go" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -324,7 +325,11 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest return nil, err } - ec2Client, err := catalogState.EC2Client(catalogAttributes.Region) + opts := []ec2Option{} + if catalogAttributes.Region != "" { + opts = append(opts, WithRegion(catalogAttributes.Region)) + } + ec2Client, err := catalogState.EC2Client(ctx, opts...) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err) } @@ -334,12 +339,12 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err) } - _, err = ec2Client.DescribeInstances(input) + _, err = ec2Client.DescribeInstances(ctx, input) if err == nil { return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found") } - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" { + if awsErr, ok := err.(awserr.APIError); ok && awsErr.ErrorCode() == "DryRunOperation" { // Success return &pb.OnCreateSetResponse{}, nil } @@ -395,7 +400,11 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest return nil, err } - ec2Client, err := catalogState.EC2Client(catalogAttributes.Region) + opts := []ec2Option{} + if catalogAttributes.Region != "" { + opts = append(opts, WithRegion(catalogAttributes.Region)) + } + ec2Client, err := catalogState.EC2Client(ctx, opts...) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err) } @@ -405,12 +414,12 @@ func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err) } - _, err = ec2Client.DescribeInstances(input) + _, err = ec2Client.DescribeInstances(ctx, input) if err == nil { return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found") } - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" { + if awsErr, ok := err.(awserr.APIError); ok && awsErr.ErrorCode() == "DryRunOperation" { // Success return &pb.OnUpdateSetResponse{}, nil } @@ -494,7 +503,11 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (* } } - ec2Client, err := catalogState.EC2Client(catalogAttributes.Region) + opts := []ec2Option{} + if catalogAttributes.Region != "" { + opts = append(opts, WithRegion(catalogAttributes.Region)) + } + ec2Client, err := catalogState.EC2Client(ctx, opts...) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err) } @@ -502,7 +515,7 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (* // Run all queries now and assemble output. var maxLen int for i, query := range queries { - output, err := ec2Client.DescribeInstances(query.Input) + output, err := ec2Client.DescribeInstances(ctx, query.Input) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "error running DescribeInstances for host set id %q: %s", query.Id, err) } @@ -556,8 +569,8 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (* }, nil } -func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) { - var filters []*ec2.Filter +func buildFilters(attrs *SetAttributes) ([]types.Filter, error) { + var filters []types.Filter var foundStateFilter bool for _, filterAttr := range attrs.Filters { // Each filter arrives in "k=v1,v2" format. First, split on equal sign. @@ -576,14 +589,9 @@ func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) { foundStateFilter = true } - var filterValues []*string - for _, val := range strings.Split(filterValue, ",") { - filterValues = append(filterValues, aws.String(val)) - } - - filters = append(filters, &ec2.Filter{ + filters = append(filters, types.Filter{ Name: aws.String(filterKey), - Values: filterValues, + Values: strings.Split(filterValue, ","), }) } @@ -592,9 +600,9 @@ func buildFilters(attrs *SetAttributes) ([]*ec2.Filter, error) { // instance-state-name = ["running"] to the filter set. This // ensures that we filter on running instances only at the API // side, saving time when processing results. - filters = append(filters, &ec2.Filter{ + filters = append(filters, types.Filter{ Name: aws.String("instance-state-name"), - Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}), + Values: []string{string(types.InstanceStateNameRunning)}, }) } @@ -616,97 +624,49 @@ func buildDescribeInstancesInput(attrs *SetAttributes, dryRun bool) (*ec2.Descri // awsInstanceToHost processes data from an ec2.Instance and returns // a ListHostsResponseHost with the ID and network addresses // populated. -func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error) { - // Integrity check: some fields should always be non-nil. Check - // those. - if instance == nil { - return nil, errors.New("response integrity error: missing instance entry") - } - - if aws.StringValue(instance.InstanceId) == "" { +func awsInstanceToHost(instance types.Instance) (*pb.ListHostsResponseHost, error) { + if aws.ToString(instance.InstanceId) == "" { return nil, errors.New("response integrity error: missing instance id") } result := new(pb.ListHostsResponseHost) // External ID is the instance ID. - result.ExternalId = aws.StringValue(instance.InstanceId) + result.ExternalId = aws.ToString(instance.InstanceId) // External Name is the AWS "Name" tag, if it exists. for _, t := range instance.Tags { - if aws.StringValue(t.Key) == ConstInstanceNameTagKey { - result.ExternalName = aws.StringValue(t.Value) + if aws.ToString(t.Key) == ConstInstanceNameTagKey { + result.ExternalName = aws.ToString(t.Value) break } } - // First IP address/dns name are always the private fields if they - // are populated - if aws.StringValue(instance.PrivateIpAddress) != "" { - result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PrivateIpAddress)) - } - if aws.StringValue(instance.PrivateDnsName) != "" { - result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PrivateDnsName)) - } - - // Public IP address/dns names are next - if aws.StringValue(instance.PublicIpAddress) != "" { - result.IpAddresses = append(result.IpAddresses, aws.StringValue(instance.PublicIpAddress)) - } - if aws.StringValue(instance.PublicDnsName) != "" { - result.DnsNames = append(result.DnsNames, aws.StringValue(instance.PublicDnsName)) - } + result.IpAddresses = appendDistinct(result.IpAddresses, instance.PrivateIpAddress, instance.PublicIpAddress) + result.DnsNames = appendDistinct(result.DnsNames, instance.PrivateDnsName, instance.PublicDnsName) // Now go through all of the interfaces and log the IP address of // every interface. for _, iface := range instance.NetworkInterfaces { - if iface == nil { - // Probably will never happen, but just in case - return nil, errors.New("response integrity error: interface entry is nil") - } - // Populate default IP addresses/DNS name similar to how we do // for the entire instance. - if aws.StringValue(iface.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress)) { - result.IpAddresses = append(result.IpAddresses, aws.StringValue(iface.PrivateIpAddress)) - } - if aws.StringValue(iface.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(iface.PrivateDnsName)) { - result.DnsNames = append(result.DnsNames, aws.StringValue(iface.PrivateDnsName)) - } + result.IpAddresses = appendDistinct(result.IpAddresses, iface.PrivateIpAddress) + result.DnsNames = appendDistinct(result.DnsNames, iface.PrivateDnsName) // Iterate through the private IP addresses and log the // information. for _, addr := range iface.PrivateIpAddresses { - if addr == nil { - return nil, errors.New("response integrity error: interface address entry is nil") - } - - // Add private address/dns name if they have not been added yet - if aws.StringValue(addr.PrivateIpAddress) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress)) { - result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.PrivateIpAddress)) - } - if aws.StringValue(addr.PrivateDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.PrivateDnsName)) { - result.DnsNames = append(result.DnsNames, aws.StringValue(addr.PrivateDnsName)) - } - - // Add public address/dns name if they have not been added yet - if addr.Association != nil && aws.StringValue(addr.Association.PublicIp) != "" && !stringInSlice(result.IpAddresses, aws.StringValue(addr.Association.PublicIp)) { - result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Association.PublicIp)) - } - if addr.Association != nil && aws.StringValue(addr.Association.PublicDnsName) != "" && !stringInSlice(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName)) { - result.DnsNames = append(result.DnsNames, aws.StringValue(addr.Association.PublicDnsName)) + result.IpAddresses = appendDistinct(result.IpAddresses, addr.PrivateIpAddress) + result.DnsNames = appendDistinct(result.DnsNames, addr.PrivateDnsName) + if addr.Association != nil { + result.IpAddresses = appendDistinct(result.IpAddresses, addr.Association.PublicIp) + result.DnsNames = appendDistinct(result.DnsNames, addr.Association.PublicDnsName) } } // Add the IPv6 addresses. for _, addr := range iface.Ipv6Addresses { - if addr == nil { - continue - } - - if addr.Ipv6Address != nil && aws.StringValue(addr.Ipv6Address) != "" { - result.IpAddresses = append(result.IpAddresses, aws.StringValue(addr.Ipv6Address)) - } + result.IpAddresses = appendDistinct(result.IpAddresses, addr.Ipv6Address) } } @@ -714,12 +674,25 @@ func awsInstanceToHost(instance *ec2.Instance) (*pb.ListHostsResponseHost, error return result, nil } +// appendDistinct will append the elements to the slice +// if an element is not nil, empty, and does not exist in slice. +func appendDistinct(slice []string, elems ...*string) []string { + for _, e := range elems { + value := aws.ToString(e) + if value == "" || stringInSlice(slice, value) { + continue + } + slice = append(slice, value) + } + return slice +} + +// stringInSlice returns true if the slice contains the given element. func stringInSlice(s []string, x string) bool { for _, y := range s { if x == y { return true } } - return false } diff --git a/plugin/service/host/plugin_test.go b/plugin/service/host/plugin_test.go index 77689fa..3f240cb 100644 --- a/plugin/service/host/plugin_test.go +++ b/plugin/service/host/plugin_test.go @@ -10,8 +10,9 @@ import ( "sort" "testing" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" "github.com/hashicorp/boundary-plugin-aws/internal/credential" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" @@ -1566,9 +1567,9 @@ func TestPluginListHostsErr(t *testing.T) { nil, testMockEC2WithDescribeInstancesOutput( &ec2.DescribeInstancesOutput{ - Reservations: []*ec2.Reservation{ + Reservations: []types.Reservation{ { - Instances: []*ec2.Instance{ + Instances: []types.Instance{ { // Blank so we error out }, @@ -1603,7 +1604,7 @@ func TestBuildFilters(t *testing.T) { cases := []struct { name string in map[string]any - expected []*ec2.Filter + expected []types.Filter expectedErrContains string }{ { @@ -1611,14 +1612,14 @@ func TestBuildFilters(t *testing.T) { in: map[string]any{ ConstDescribeInstancesFilters: []any{"foo=bar"}, }, - expected: []*ec2.Filter{ + expected: []types.Filter{ { Name: aws.String("foo"), - Values: aws.StringSlice([]string{"bar"}), + Values: []string{"bar"}, }, { Name: aws.String("instance-state-name"), - Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}), + Values: []string{string(types.InstanceStateNameRunning)}, }, }, }, @@ -1630,14 +1631,14 @@ func TestBuildFilters(t *testing.T) { "instance-state-name=static", }, }, - expected: []*ec2.Filter{ + expected: []types.Filter{ { Name: aws.String("foo"), - Values: aws.StringSlice([]string{"bar"}), + Values: []string{"bar"}, }, { Name: aws.String("instance-state-name"), - Values: aws.StringSlice([]string{"static"}), + Values: []string{"static"}, }, }, }, @@ -1648,14 +1649,14 @@ func TestBuildFilters(t *testing.T) { "foo=bar,baz", }, }, - expected: []*ec2.Filter{ + expected: []types.Filter{ { Name: aws.String("foo"), - Values: aws.StringSlice([]string{"bar", "baz"}), + Values: []string{"bar", "baz"}, }, { Name: aws.String("instance-state-name"), - Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}), + Values: []string{string(types.InstanceStateNameRunning)}, }, }, }, @@ -1664,10 +1665,10 @@ func TestBuildFilters(t *testing.T) { in: map[string]any{ ConstDescribeInstancesFilters: []any{}, }, - expected: []*ec2.Filter{ + expected: []types.Filter{ { Name: aws.String("instance-state-name"), - Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}), + Values: []string{string(types.InstanceStateNameRunning)}, }, }, }, @@ -1714,14 +1715,14 @@ func TestBuildDescribeInstancesInput(t *testing.T) { dryRun: true, expected: &ec2.DescribeInstancesInput{ DryRun: aws.Bool(true), - Filters: []*ec2.Filter{ + Filters: []types.Filter{ { Name: aws.String("foo"), - Values: aws.StringSlice([]string{"bar"}), + Values: []string{"bar"}, }, { Name: aws.String("instance-state-name"), - Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}), + Values: []string{string(types.InstanceStateNameRunning)}, }, }, }, @@ -1736,14 +1737,14 @@ func TestBuildDescribeInstancesInput(t *testing.T) { dryRun: false, expected: &ec2.DescribeInstancesInput{ DryRun: aws.Bool(false), - Filters: []*ec2.Filter{ + Filters: []types.Filter{ { Name: aws.String("foo"), - Values: aws.StringSlice([]string{"bar"}), + Values: []string{"bar"}, }, { Name: aws.String("instance-state-name"), - Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}), + Values: []string{string(types.InstanceStateNameRunning)}, }, }, }, @@ -1775,55 +1776,30 @@ func TestBuildDescribeInstancesInput(t *testing.T) { func TestAwsInstanceToHost(t *testing.T) { cases := []struct { name string - instance *ec2.Instance + instance types.Instance expected *pb.ListHostsResponseHost expectedErr string }{ - { - name: "nil instance", - instance: nil, - expectedErr: "response integrity error: missing instance entry", - }, { name: "missing instance id", - instance: &ec2.Instance{}, + instance: types.Instance{}, expectedErr: "response integrity error: missing instance id", }, - { - name: "nil interface entry", - instance: &ec2.Instance{ - InstanceId: aws.String("foobar"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{nil}, - }, - expectedErr: "response integrity error: interface entry is nil", - }, - { - name: "nil interface address entry", - instance: &ec2.Instance{ - InstanceId: aws.String("foobar"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ - { - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{nil}, - }, - }, - }, - expectedErr: "response integrity error: interface address entry is nil", - }, { name: "good, single IP w/public addr", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), PublicIpAddress: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), }, @@ -1842,15 +1818,15 @@ func TestAwsInstanceToHost(t *testing.T) { }, { name: "good, private", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), @@ -1867,17 +1843,17 @@ func TestAwsInstanceToHost(t *testing.T) { }, { name: "good, multiple interfaces", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), PublicIpAddress: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.2"), PrivateDnsName: aws.String("test2.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { PrivateIpAddress: aws.String("10.0.0.2"), PrivateDnsName: aws.String("test2.example.internal"), @@ -1887,9 +1863,9 @@ func TestAwsInstanceToHost(t *testing.T) { { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), }, @@ -1908,19 +1884,19 @@ func TestAwsInstanceToHost(t *testing.T) { }, { name: "good, multiple public interfaces", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), PublicIpAddress: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.2"), PrivateDnsName: aws.String("test2.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.2"), PublicDnsName: aws.String("test2.example.com"), }, @@ -1932,9 +1908,9 @@ func TestAwsInstanceToHost(t *testing.T) { { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), }, @@ -1953,19 +1929,19 @@ func TestAwsInstanceToHost(t *testing.T) { }, { name: "good, multiple addresses on single interface", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), PublicIpAddress: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), }, @@ -1988,25 +1964,25 @@ func TestAwsInstanceToHost(t *testing.T) { }, { name: "good, single IP w/public addr and IPv6", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), PublicIpAddress: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.1"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.1"), }, PrivateIpAddress: aws.String("10.0.0.1"), }, }, - Ipv6Addresses: []*ec2.InstanceIpv6Address{ - nil, // Just coverage for nil assertion which is skipped + Ipv6Addresses: []types.InstanceIpv6Address{ + {Ipv6Address: nil}, // Just coverage for nil assertion which is skipped {Ipv6Address: aws.String("some::fake::address")}, }, }, @@ -2020,24 +1996,24 @@ func TestAwsInstanceToHost(t *testing.T) { }, { name: "good, single IP w/public addr and external name", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), PublicIpAddress: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), - Tags: []*ec2.Tag{ + Tags: []types.Tag{ {Key: aws.String("Name"), Value: aws.String("test-instance-actual-name")}, // The tag name is "Name", not "name". {Key: aws.String("name"), Value: aws.String("test-instance-fake-name")}, {Key: aws.String("contains-Name"), Value: aws.String("test-instance-contains-name")}, }, - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), }, @@ -2057,24 +2033,24 @@ func TestAwsInstanceToHost(t *testing.T) { }, { name: "good, single IP w/public addr and external name", - instance: &ec2.Instance{ + instance: types.Instance{ InstanceId: aws.String("foobar"), PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), PublicIpAddress: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), - Tags: []*ec2.Tag{ + Tags: []types.Tag{ {Key: aws.String("Name"), Value: aws.String("test-instance-actual-name")}, // The tag name is "Name", not "name". {Key: aws.String("name"), Value: aws.String("test-instance-fake-name")}, {Key: aws.String("contains-Name"), Value: aws.String("test-instance-contains-name")}, }, - NetworkInterfaces: []*ec2.InstanceNetworkInterface{ + NetworkInterfaces: []types.InstanceNetworkInterface{ { PrivateIpAddress: aws.String("10.0.0.1"), PrivateDnsName: aws.String("test.example.internal"), - PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{ + PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Association: &ec2.InstanceNetworkInterfaceAssociation{ + Association: &types.InstanceNetworkInterfaceAssociation{ PublicIp: aws.String("1.1.1.1"), PublicDnsName: aws.String("test.example.com"), }, @@ -2109,3 +2085,55 @@ func TestAwsInstanceToHost(t *testing.T) { }) } } + +func TestAppendDistinct(t *testing.T) { + cases := []struct { + name string + slice []string + elems []*string + expected []string + }{ + { + name: "empty slice, empty elems", + slice: []string{}, + elems: []*string{}, + expected: []string{}, + }, + { + name: "empty elems", + slice: []string{"bar"}, + elems: []*string{}, + expected: []string{"bar"}, + }, + { + name: "skip nil elem", + slice: []string{"bar"}, + elems: []*string{nil}, + expected: []string{"bar"}, + }, + { + name: "skip empty elem", + slice: []string{"bar"}, + elems: []*string{aws.String("")}, + expected: []string{"bar"}, + }, + { + name: "skip duplicate elem", + slice: []string{"bar"}, + elems: []*string{ + aws.String("bar"), + aws.String("foo"), + aws.String("foo"), + }, + expected: []string{"bar", "foo"}, + }, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + actual := appendDistinct(tc.slice, tc.elems...) + require.ElementsMatch(actual, tc.expected) + }) + } +} diff --git a/plugin/service/host/state.go b/plugin/service/host/state.go index 2acc223..534a8a4 100644 --- a/plugin/service/host/state.go +++ b/plugin/service/host/state.go @@ -4,21 +4,26 @@ package host import ( + "context" "errors" "fmt" "net" "net/http" "time" - "github.com/aws/aws-sdk-go/aws" - "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" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/ec2" "github.com/hashicorp/boundary-plugin-aws/internal/credential" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" "google.golang.org/protobuf/types/known/structpb" ) +type EC2API interface { + DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) +} + var customClient = &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, @@ -30,10 +35,6 @@ var customClient = &http.Client{ }, } -type ec2APIFunc func(*session.Session, ...*aws.Config) (ec2iface.EC2API, error) - -type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error - type awsCatalogPersistedState struct { *credential.AwsCredentialPersistedState @@ -41,6 +42,10 @@ type awsCatalogPersistedState struct { testEC2APIFunc ec2APIFunc } +type ec2APIFunc func(...aws.Config) (EC2API, error) + +type awsCatalogPersistedStateOption func(s *awsCatalogPersistedState) error + func withTestEC2APIFunc(f ec2APIFunc) awsCatalogPersistedStateOption { return func(s *awsCatalogPersistedState) error { if s.testEC2APIFunc != nil { @@ -83,22 +88,75 @@ func (s *awsCatalogPersistedState) toProto() (*pb.HostCatalogPersisted, error) { // EC2Client returns a configured EC2 client based on the session // information stored in the state. -func (s *awsCatalogPersistedState) EC2Client(region string) (ec2iface.EC2API, error) { +func (s *awsCatalogPersistedState) EC2Client(ctx context.Context, opt ...ec2Option) (EC2API, error) { + opts, err := getOpts(opt...) + if err != nil { + return nil, fmt.Errorf("error parsing options when fetching EC2 client: %w", err) + } + + awsConfigOpts := []func(*config.LoadOptions) error{} sess, err := s.GetSession() if err != nil { return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err) } - - if s.testEC2APIFunc != nil { - return s.testEC2APIFunc(sess, aws.NewConfig().WithRegion(region)) + creds, err := sess.Config.Credentials.Get() + if err != nil { + return nil, fmt.Errorf("error getting AWS session when fetching EC2 client: %w", err) + } + customCredentials := config.WithCredentialsProvider( + credentials.NewStaticCredentialsProvider(creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken), + ) + awsConfigOpts = append(awsConfigOpts, customCredentials) + if sess.Config.Region != nil { + awsConfigOpts = append(awsConfigOpts, config.WithRegion(*sess.Config.Region)) + } + if opts.withRegion != "" { + awsConfigOpts = append(awsConfigOpts, config.WithRegion(opts.withRegion)) + } + cfg, err := config.LoadDefaultConfig(ctx, awsConfigOpts...) + if err != nil { + return nil, fmt.Errorf("error loading aws configuration when fetching EC2 client: %w", err) } - cfg := aws.NewConfig() - if region != "" { - cfg = cfg.WithRegion(region) + if s.testEC2APIFunc != nil { + return s.testEC2APIFunc(cfg) } cfg.HTTPClient = customClient - return ec2.New(sess, cfg), nil + return ec2.NewFromConfig(cfg), nil +} + +// getOpts iterates the inbound ec2Options and returns a struct +func getOpts(opt ...ec2Option) (ec2Options, error) { + opts := getDefaultEC2Options() + for _, o := range opt { + if o == nil { + continue + } + if err := o(&opts); err != nil { + return ec2Options{}, err + } + } + return opts, nil +} + +// ec2Option - how ec2Options are passed as arguments +type ec2Option func(*ec2Options) error + +// options = how options are represented +type ec2Options struct { + withRegion string +} + +func getDefaultEC2Options() ec2Options { + return ec2Options{} +} + +// WithRegion contains the region to use +func WithRegion(with string) ec2Option { + return func(o *ec2Options) error { + o.withRegion = with + return nil + } } diff --git a/plugin/service/host/state_test.go b/plugin/service/host/state_test.go index b1a31bb..a9c537e 100644 --- a/plugin/service/host/state_test.go +++ b/plugin/service/host/state_test.go @@ -4,6 +4,7 @@ package host import ( + "context" "testing" "github.com/hashicorp/boundary-plugin-aws/internal/credential" @@ -25,7 +26,7 @@ func TestAwsCatalogPersistedStateEC2Client(t *testing.T) { ) require.NoError(err) - clientRaw, err := state.EC2Client("us-west-2") + clientRaw, err := state.EC2Client(context.Background(), WithRegion("us-west-2")) require.NoError(err) require.NotNil(clientRaw) client, ok := clientRaw.(*testMockEC2) diff --git a/plugin/service/host/testing.go b/plugin/service/host/testing.go index 8f5e72e..e17a4f9 100644 --- a/plugin/service/host/testing.go +++ b/plugin/service/host/testing.go @@ -4,10 +4,11 @@ package host import ( - "github.com/aws/aws-sdk-go/aws" - "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" + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/aws/aws-sdk-go-v2/service/ec2/types" ) const ( @@ -29,10 +30,11 @@ func (s *testMockEC2State) Reset() { } type testMockEC2 struct { - ec2iface.EC2API + EC2API + + State *testMockEC2State + Region string - State *testMockEC2State - Region string DescribeInstancesOutput *ec2.DescribeInstancesOutput DescribeInstancesError error } @@ -54,7 +56,7 @@ func testMockEC2WithDescribeInstancesError(e error) testMockEC2Option { } func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc { - return func(sess *session.Session, cfgs ...*aws.Config) (ec2iface.EC2API, error) { + return func(cfgs ...aws.Config) (EC2API, error) { m := &testMockEC2{ State: state, } @@ -67,14 +69,17 @@ func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFu for _, cfg := range cfgs { // Last region takes precedence - m.Region = *cfg.Region + if cfg.Region != "" { + m.Region = cfg.Region + } + } return m, nil } } -func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) { +func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error) { if m.State != nil { m.State.DescribeInstancesCalled = true m.State.DescribeInstancesInputParams = input @@ -87,7 +92,7 @@ func (m *testMockEC2) DescribeInstances(input *ec2.DescribeInstancesInput) (*ec2 return m.DescribeInstancesOutput, nil } -type ec2FilterSorter []*ec2.Filter +type ec2FilterSorter []types.Filter func (s ec2FilterSorter) Len() int { return len(s) } func (s ec2FilterSorter) Less(i, j int) bool { return *s[i].Name < *s[j].Name } diff --git a/plugin/service/storage/state_test.go b/plugin/service/storage/state_test.go index 2a139b9..1ec5af6 100644 --- a/plugin/service/storage/state_test.go +++ b/plugin/service/storage/state_test.go @@ -7,7 +7,7 @@ import ( "context" "testing" - "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/hashicorp/boundary-plugin-aws/internal/credential" "github.com/stretchr/testify/require" )