Files
threshold-plugin-aws/plugin/plugin_test.go
T
Jeff Mitchell 1c7bacafa7 Handle the case where an incoming filter is a string (#4)
Ideally we'd ensure that an incoming filter is a string, but some
clients, such as the CLI, don't have a good way to require this without
it being very onerous to the user.

Eventually we can handle this via allowing the plugin to declare a
schema and perform type validation in the handler, but for now, work
around this within the plugin.
2021-11-12 19:47:46 -05:00

1812 lines
48 KiB
Go

package plugin
import (
"context"
"errors"
"fmt"
"sort"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
func TestPluginOnCreateCatalogErr(t *testing.T) {
cases := []struct {
name string
req *pb.OnCreateCatalogRequest
opts []awsCatalogPersistedStateOption
expectedErr string
}{
{
name: "nil catalog",
req: &pb.OnCreateCatalogRequest{},
expectedErr: "catalog is nil",
},
{
name: "nil secrets",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{},
},
expectedErr: "secrets are required",
},
{
name: "nil attributes",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErr: "attributes are required",
},
{
name: "invalid region",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attributes: mustStruct(map[string]interface{}{
constRegion: "foobar",
}),
},
},
expectedErr: "not a valid region: foobar",
},
{
name: "missing access key ID",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
},
expectedErr: "missing required value \"access_key_id\"",
},
{
name: "missing secret access key",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
}),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
},
expectedErr: "missing required value \"secret_access_key\"",
},
{
name: "invalid value for skipping rotation",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
}),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
constDisableCredentialRotation: "sure",
}),
},
},
expectedErr: "unexpected type for value \"disable_credential_rotation\": want bool, got string",
},
{
name: "persisted state setup error",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
}),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
},
opts: []awsCatalogPersistedStateOption{
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErr: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
},
{
name: "rotation error",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
}),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
),
),
}),
},
expectedErr: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
},
{
name: "validation error",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
}),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
constDisableCredentialRotation: true,
}),
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(
awsutil.NewMockSTS(
awsutil.WithGetCallerIdentityError(errors.New(testGetCallerIdentityErr)),
),
),
}),
},
expectedErr: fmt.Sprintf("error during credential validation: error validating credentials: %s", testGetCallerIdentityErr),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &AwsPlugin{
testStateOpts: tc.opts,
}
_, err := p.OnCreateCatalog(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr)
})
}
}
func TestPluginOnUpdateCatalogErr(t *testing.T) {
cases := []struct {
name string
req *pb.OnUpdateCatalogRequest
opts []awsCatalogPersistedStateOption
expectedErr string
}{
{
name: "nil new catalog",
req: &pb.OnUpdateCatalogRequest{},
expectedErr: "new catalog is nil",
},
{
name: "nil new attributes",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErr: "new catalog missing attributes",
},
{
name: "invalid region",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "foobar",
}),
},
},
expectedErr: "not a valid region: foobar",
},
{
name: "persisted state setup error",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
opts: []awsCatalogPersistedStateOption{
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
},
{
name: "invalid value for credential rotation",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
constDisableCredentialRotation: "sure",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "unexpected type for value \"disable_credential_rotation\": want bool, got string",
},
{
name: "cannot disable rotation for already rotated credentials",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
constDisableCredentialRotation: true,
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "cannot disable rotation for already-rotated credentials",
},
{
name: "updating secrets, missing access key id",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "missing required value \"access_key_id\"",
},
{
name: "updating secrets, missing secret access key",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
}),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "missing required value \"secret_access_key\"",
},
{
name: "updating secrets, replace error",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "onetwo",
constSecretAccessKey: "threefour",
}),
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
constDisableCredentialRotation: true,
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
}),
},
expectedErr: fmt.Sprintf("error attempting to replace credentials: error deleting old access key: %s", testDeleteAccessKeyErr),
},
{
name: "rotation error",
req: &pb.OnUpdateCatalogRequest{
NewCatalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
}),
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
),
),
}),
},
expectedErr: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &AwsPlugin{
testStateOpts: tc.opts,
}
_, err := p.OnUpdateCatalog(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr)
})
}
}
func TestPluginOnDeleteCatalogErr(t *testing.T) {
cases := []struct {
name string
req *pb.OnDeleteCatalogRequest
opts []awsCatalogPersistedStateOption
expectedErr string
}{
{
name: "persisted state setup error",
req: &pb.OnDeleteCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
opts: []awsCatalogPersistedStateOption{
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
},
{
name: "delete error",
req: &pb.OnDeleteCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
}),
},
expectedErr: fmt.Sprintf("error removing rotated credentials during catalog deletion: error deleting old access key: %s", testDeleteAccessKeyErr),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &AwsPlugin{
testStateOpts: tc.opts,
}
_, err := p.OnDeleteCatalog(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr)
})
}
}
func TestPluginOnCreateSetErr(t *testing.T) {
cases := []struct {
name string
req *pb.OnCreateSetRequest
opts []awsCatalogPersistedStateOption
expectedErr string
}{
{
name: "nil catalog",
req: &pb.OnCreateSetRequest{},
expectedErr: "catalog is nil",
},
{
name: "nil catalog attributes",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErr: "catalog missing attributes",
},
{
name: "persisted state setup error",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
opts: []awsCatalogPersistedStateOption{
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
},
{
name: "invalid region",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "foobar",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "catalog validation error: not a valid region: foobar",
},
{
name: "nil set",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "set is nil",
},
{
name: "nil attributes in set",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Set: &hostsets.HostSet{},
},
expectedErr: "set missing attributes",
},
{
name: "client load error",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Set: &hostsets.HostSet{
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErr: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr),
},
{
name: "DescribeInstances error",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Set: &hostsets.HostSet{
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
opts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
expectedErr: fmt.Sprintf("error performing dry run of DescribeInstances: %s", testDescribeInstancesError),
},
{
name: "DescribeInstances non-error array filter",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Set: &hostsets.HostSet{
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
opts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
)),
},
expectedErr: "query error: DescribeInstances DryRun should have returned error, but none was found",
},
{
name: "DescribeInstances non-error string filter",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Set: &hostsets.HostSet{
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: "tag-key=foo",
}),
},
},
opts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
)),
},
expectedErr: "query error: DescribeInstances DryRun should have returned error, but none was found",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &AwsPlugin{
testStateOpts: tc.opts,
}
_, err := p.OnCreateSet(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr)
})
}
}
func TestPluginOnUpdateSetErr(t *testing.T) {
cases := []struct {
name string
req *pb.OnUpdateSetRequest
opts []awsCatalogPersistedStateOption
expectedErr string
}{
{
name: "nil catalog",
req: &pb.OnUpdateSetRequest{},
expectedErr: "catalog is nil",
},
{
name: "nil catalog attributes",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErr: "catalog missing attributes",
},
{
name: "persisted state setup error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
opts: []awsCatalogPersistedStateOption{
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
},
{
name: "invalid region",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "foobar",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "catalog validation error: not a valid region: foobar",
},
{
name: "nil set",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "new set is nil",
},
{
name: "nil attributes in set",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
NewSet: &hostsets.HostSet{},
},
expectedErr: "new set missing attributes",
},
{
name: "client load error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
NewSet: &hostsets.HostSet{
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErr: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr),
},
{
name: "DescribeInstances error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
NewSet: &hostsets.HostSet{
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
opts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
expectedErr: fmt.Sprintf("error performing dry run of DescribeInstances: %s", testDescribeInstancesError),
},
{
name: "DescribeInstances non-error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
NewSet: &hostsets.HostSet{
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
opts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
)),
},
expectedErr: "query error: DescribeInstances DryRun should have returned error, but none was found",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &AwsPlugin{
testStateOpts: tc.opts,
}
_, err := p.OnUpdateSet(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr)
})
}
}
func TestPluginListHostsErr(t *testing.T) {
cases := []struct {
name string
req *pb.ListHostsRequest
opts []awsCatalogPersistedStateOption
expectedErr string
}{
{
name: "nil catalog",
req: &pb.ListHostsRequest{},
expectedErr: "catalog is nil",
},
{
name: "nil catalog attributes",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErr: "catalog missing attributes",
},
{
name: "persisted state setup error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
opts: []awsCatalogPersistedStateOption{
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
},
{
name: "invalid region",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "foobar",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "catalog validation error: not a valid region: foobar",
},
{
name: "nil sets",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
},
expectedErr: "sets is nil",
},
{
name: "set missing id",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Sets: []*hostsets.HostSet{&hostsets.HostSet{}},
},
expectedErr: "set missing id",
},
{
name: "set missing attributes",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Sets: []*hostsets.HostSet{
&hostsets.HostSet{
Id: "foobar",
},
},
},
expectedErr: "set missing attributes",
},
{
name: "client load error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Sets: []*hostsets.HostSet{
{
Id: "foobar",
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
},
opts: []awsCatalogPersistedStateOption{
withStateTestOpts([]awsutil.Option{
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErr: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr),
},
{
name: "DescribeInstances error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Sets: []*hostsets.HostSet{
{
Id: "foobar",
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
},
opts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
expectedErr: fmt.Sprintf("error running DescribeInstances for host set id \"foobar\": %s", testDescribeInstancesError),
},
{
name: "awsInstanceToHost error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attributes: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
},
Persisted: &pb.HostCatalogPersisted{
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
Sets: []*hostsets.HostSet{
{
Id: "foobar",
Attributes: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{"tag-key=foo"},
}),
},
},
},
opts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(
&ec2.DescribeInstancesOutput{
Reservations: []*ec2.Reservation{
&ec2.Reservation{
Instances: []*ec2.Instance{
&ec2.Instance{
// Blank so we error out
},
},
},
},
},
),
)),
},
expectedErr: "error processing host results for host set id \"foobar\": response integrity error: missing instance id",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &AwsPlugin{
testStateOpts: tc.opts,
}
_, err := p.ListHosts(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr)
})
}
}
func TestGetStringValue(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
key string
required bool
expected string
expectedErr string
}{
{
name: "required missing",
in: mustStruct(map[string]interface{}{}),
key: "foo",
required: true,
expectedErr: "missing required value \"foo\"",
},
{
name: "optional missing",
in: mustStruct(map[string]interface{}{}),
key: "foo",
expected: "",
},
{
name: "non-string value",
in: mustStruct(map[string]interface{}{
"foo": 1,
}),
key: "foo",
expectedErr: "unexpected type for value \"foo\": want string, got float64",
},
{
name: "required empty",
in: mustStruct(map[string]interface{}{
"foo": "",
}),
key: "foo",
required: true,
expectedErr: "value \"foo\" cannot be empty",
},
{
name: "good",
in: mustStruct(map[string]interface{}{
"foo": "bar",
}),
key: "foo",
expected: "bar",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := getStringValue(tc.in, tc.key, tc.required)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestGetBoolValue(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
key string
required bool
expected bool
expectedErr string
}{
{
name: "required missing",
in: mustStruct(map[string]interface{}{}),
key: "foo",
required: true,
expectedErr: "missing required value \"foo\"",
},
{
name: "optional missing",
in: mustStruct(map[string]interface{}{}),
key: "foo",
expected: false,
},
{
name: "non-bool value",
in: mustStruct(map[string]interface{}{
"foo": "bar",
}),
key: "foo",
expectedErr: "unexpected type for value \"foo\": want bool, got string",
},
{
name: "good",
in: mustStruct(map[string]interface{}{
"foo": true,
}),
key: "foo",
expected: true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := getBoolValue(tc.in, tc.key, tc.required)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestGetTimeValue(t *testing.T) {
staticTime := time.Now()
cases := []struct {
name string
in *structpb.Struct
key string
expected time.Time
expectedErr string
}{
{
name: "missing",
in: mustStruct(map[string]interface{}{}),
key: "foo",
expected: time.Time{},
},
{
name: "non-time value",
in: mustStruct(map[string]interface{}{
"foo": 1,
}),
key: "foo",
expectedErr: "unexpected type for value \"foo\": want string, got float64",
},
{
name: "bad parse",
in: mustStruct(map[string]interface{}{
"foo": "bar",
}),
key: "foo",
expectedErr: "could not parse time in value \"foo\": parsing time \"bar\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"bar\" as \"2006\"",
},
{
name: "good",
in: mustStruct(map[string]interface{}{
"foo": staticTime.Format(time.RFC3339Nano),
}),
key: "foo",
expected: func() time.Time {
u, err := time.Parse(time.RFC3339Nano, staticTime.Format(time.RFC3339Nano))
if err != nil {
panic(err)
}
return u
}(),
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := getTimeValue(tc.in, tc.key)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestGetSetAttributes(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
expected *SetAttributes
expectedErrContains string
}{
{
name: "missing",
in: mustStruct(map[string]interface{}{}),
expected: &SetAttributes{},
},
{
name: "non-string-slice value",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: "zip=foo,bar",
}),
expected: &SetAttributes{
Filters: []string{"zip=foo,bar"},
},
},
{
name: "bad filter element value",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{1},
}),
expectedErrContains: "expected type 'string', got unconvertible type 'float64'",
},
{
name: "good",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{
"foo=bar",
"zip=zap",
},
}),
expected: &SetAttributes{
Filters: []string{"foo=bar", "zip=zap"},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := getSetAttributes(tc.in)
if tc.expectedErrContains != "" {
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestGetValidateRegionValue(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
key string
expected string
expectedErr string
}{
{
name: "missing",
in: mustStruct(map[string]interface{}{}),
key: "region",
expectedErr: fmt.Sprintf("missing required value %q", constRegion),
},
{
name: "invalid region",
in: mustStruct(map[string]interface{}{
constRegion: "foobar",
}),
expectedErr: "not a valid region: foobar",
},
{
name: "good",
in: mustStruct(map[string]interface{}{
constRegion: "us-west-2",
}),
expected: "us-west-2",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := getValidateRegionValue(tc.in)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestBuildFilters(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
expected []*ec2.Filter
expectedErrContains string
}{
{
name: "good without instance-state-name",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{
"foo=bar",
},
}),
expected: []*ec2.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
},
},
},
{
name: "good with instance-state-name",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{
"foo=bar",
"instance-state-name=static",
},
}),
expected: []*ec2.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{"static"}),
},
},
},
{
name: "good with multiple values",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{
"foo=bar,baz",
},
}),
expected: []*ec2.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar", "baz"}),
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
},
},
},
{
name: "empty filter set",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{},
}),
expected: []*ec2.Filter{
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
attrs, err := getSetAttributes(tc.in)
require.NoError(err)
actual, err := buildFilters(attrs)
if tc.expectedErrContains != "" {
require.Contains(err.Error(), tc.expectedErrContains)
return
}
require.NoError(err)
sort.Sort(ec2FilterSorter(actual))
require.Equal(tc.expected, actual)
})
}
}
func TestBuildDescribeInstancesInput(t *testing.T) {
cases := []struct {
name string
in *structpb.Struct
dryRun bool
expected *ec2.DescribeInstancesInput
expectedErr string
}{
{
name: "good, dry run",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{
"foo=bar",
},
}),
dryRun: true,
expected: &ec2.DescribeInstancesInput{
DryRun: aws.Bool(true),
Filters: []*ec2.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
},
},
},
},
{
name: "good, real run",
in: mustStruct(map[string]interface{}{
constDescribeInstancesFilters: []interface{}{
"foo=bar",
},
}),
dryRun: false,
expected: &ec2.DescribeInstancesInput{
DryRun: aws.Bool(false),
Filters: []*ec2.Filter{
{
Name: aws.String("foo"),
Values: aws.StringSlice([]string{"bar"}),
},
{
Name: aws.String("instance-state-name"),
Values: aws.StringSlice([]string{ec2.InstanceStateNameRunning}),
},
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
attrs, err := getSetAttributes(tc.in)
require.NoError(err)
actual, err := buildDescribeInstancesInput(attrs, tc.dryRun)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestAwsInstanceToHost(t *testing.T) {
cases := []struct {
name string
instance *ec2.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{},
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{
&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{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PublicIpAddress: aws.String("1.1.1.1"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
Association: &ec2.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
},
},
{
name: "good, private",
instance: &ec2.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
PrivateIpAddress: aws.String("10.0.0.1"),
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1"},
},
},
{
name: "good, multiple interfaces",
instance: &ec2.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PublicIpAddress: aws.String("1.1.1.1"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
PrivateIpAddress: aws.String("10.0.0.2"),
},
},
},
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
Association: &ec2.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.2"},
},
},
{
name: "good, multiple public interfaces",
instance: &ec2.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PublicIpAddress: aws.String("1.1.1.1"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
Association: &ec2.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.2"),
},
PrivateIpAddress: aws.String("10.0.0.2"),
},
},
},
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
Association: &ec2.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.2", "1.1.1.2"},
},
},
{
name: "good, multiple addresses on single interface",
instance: &ec2.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PublicIpAddress: aws.String("1.1.1.1"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
Association: &ec2.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
},
&ec2.InstancePrivateIpAddress{
PrivateIpAddress: aws.String("10.0.0.2"),
},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.2"},
},
},
{
name: "good, single IP w/public addr and IPv6",
instance: &ec2.Instance{
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
PublicIpAddress: aws.String("1.1.1.1"),
NetworkInterfaces: []*ec2.InstanceNetworkInterface{
&ec2.InstanceNetworkInterface{
PrivateIpAddresses: []*ec2.InstancePrivateIpAddress{
&ec2.InstancePrivateIpAddress{
Association: &ec2.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
&ec2.InstanceIpv6Address{Ipv6Address: aws.String("some::fake::address")},
},
},
},
},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "some::fake::address"},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := awsInstanceToHost(tc.instance)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}