Files
threshold-plugin-aws/plugin/service/host/plugin_test.go
T

3480 lines
112 KiB
Go
Raw Normal View History

// Copyright IBM Corp. 2021, 2026
// SPDX-License-Identifier: MPL-2.0
package host
2021-09-22 17:00:37 -07:00
import (
"context"
2021-10-06 12:47:05 -07:00
"errors"
2021-09-24 12:47:57 -07:00
"fmt"
2021-10-07 12:40:55 -07:00
"sort"
2021-09-22 17:00:37 -07:00
"testing"
"time"
2021-09-22 17:00:37 -07:00
"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/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
2026-09-24 11:12:33 -07:00
"github.com/aws/aws-sdk-go-v2/service/sts"
stsTypes "github.com/aws/aws-sdk-go-v2/service/sts/types"
"github.com/google/go-cmp/cmp"
"repo.anhonesthost.net/CyberCoveLLC/threshold-plugin-aws/internal/credential"
awserrors "repo.anhonesthost.net/CyberCoveLLC/threshold-plugin-aws/internal/errors"
"repo.anhonesthost.net/CyberCoveLLC/threshold/sdk/pbs/controller/api/resources/hostcatalogs"
"repo.anhonesthost.net/CyberCoveLLC/threshold/sdk/pbs/controller/api/resources/hostsets"
pb "repo.anhonesthost.net/CyberCoveLLC/threshold/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
2021-09-22 17:00:37 -07:00
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/testing/protocmp"
2021-09-22 17:00:37 -07:00
"google.golang.org/protobuf/types/known/structpb"
)
2026-09-24 11:12:33 -07:00
// testRoleARN is a syntactically valid ARN used for role_arn test cases.
// All STS calls against it are handled by injected mocks — no real AWS
// account is contacted.
const testRoleARN = "arn:aws:iam::123456789012:role/BoundaryTestRole"
// validAssumeRoleOutput returns a mock AssumeRoleOutput with synthetic
// temporary credentials for use with WithSTSAPIFunc.
func validAssumeRoleOutput() *sts.AssumeRoleOutput {
return &sts.AssumeRoleOutput{
Credentials: &stsTypes.Credentials{
AccessKeyId: aws.String("ASIAmockassumedkey"),
SecretAccessKey: aws.String("mockassumedsecret"),
SessionToken: aws.String("mockassumedsessiontoken"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
},
}
}
// catalogAttrsStruct builds a catalog attributes structpb for the given
// region. roleArn is written only when non-empty. disableRotation must be
// set explicitly to avoid implicit rotation behaviour in tests.
func catalogAttrsStruct(region, roleArn string, disableRotation bool) *structpb.Struct {
fields := map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue(region),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(disableRotation),
}
if roleArn != "" {
fields[credential.ConstRoleArn] = structpb.NewStringValue(roleArn)
}
return &structpb.Struct{Fields: fields}
}
2021-09-22 17:00:37 -07:00
func TestPluginOnCreateCatalogSuccess(t *testing.T) {
tests := []struct {
name string
req *pb.OnCreateCatalogRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expRsp *pb.OnCreateCatalogResponse
}{
{
name: "usingStaticCredentials",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
expRsp: &pb.OnCreateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
},
},
},
},
},
{
name: "usingRotatedStaticCredentials",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS()),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("AKIArotated"),
SecretAccessKey: aws.String("rotated_secret"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
}),
},
expRsp: &pb.OnCreateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIArotated"),
credential.ConstSecretAccessKey: structpb.NewStringValue("rotated_secret"),
},
},
},
},
},
{
2026-09-24 11:12:33 -07:00
// role_arn set, no static keys. AssumeRole mock succeeds.
// Persisted secrets are empty — dynamic credentials are not stored.
name: "usingAssumeRole",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
2026-09-24 11:12:33 -07:00
Attributes: catalogAttrsStruct("us-east-1", testRoleARN, true),
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
2026-09-24 11:12:33 -07:00
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
awsutil.WithAssumeRoleOutput(validAssumeRoleOutput()),
)),
}),
},
expRsp: &pb.OnCreateCatalogResponse{Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &HostPlugin{
testCredStateOpts: tt.credOpts,
testCatalogStateOpts: tt.catalogOpts,
}
rsp, err := p.OnCreateCatalog(context.Background(), tt.req)
require.NoError(t, err)
delete(rsp.GetPersisted().GetSecrets().GetFields(), credential.ConstCredsLastRotatedTime)
require.Empty(t, cmp.Diff(tt.expRsp, rsp, protocmp.Transform()))
})
}
}
func TestPluginOnUpdateCatalogSuccess(t *testing.T) {
// static -> static rotated
// static -> dynamic
// static rotated -> static
// static rotated -> dynamic
// dynamic -> static
// dynamic -> static rotated
tests := []struct {
name string
req *pb.OnUpdateCatalogRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expRsp *pb.OnUpdateCatalogResponse
}{
{
name: "staticCredentialToStaticRotated",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAnewcred"),
credential.ConstSecretAccessKey: structpb.NewStringValue("newcred"),
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIApersisted"),
credential.ConstSecretAccessKey: structpb.NewStringValue("persisted"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Time{}.Format(time.RFC3339Nano)),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS()),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("AKIArotated"),
SecretAccessKey: aws.String("rotated_secret"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
}),
},
expRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIArotated"),
credential.ConstSecretAccessKey: structpb.NewStringValue("rotated_secret"),
},
},
},
},
},
{
name: "staticCredentialToAssumeRole",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIApersisted"),
credential.ConstSecretAccessKey: structpb.NewStringValue("persisted"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Time{}.Format(time.RFC3339Nano)),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
2026-09-24 11:12:33 -07:00
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
awsutil.WithAssumeRoleOutput(&sts.AssumeRoleOutput{
Credentials: &stsTypes.Credentials{
AccessKeyId: aws.String("ASIAfoobar"),
SecretAccessKey: aws.String("secretkeyfoobar"),
SessionToken: aws.String("sessiontokenfoobar"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
},
}),
)),
}),
},
expRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}},
},
},
{
name: "staticRotatedCredentialToStatic",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAnotrotated"),
credential.ConstSecretAccessKey: structpb.NewStringValue("notrotated"),
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIApersisted"),
credential.ConstSecretAccessKey: structpb.NewStringValue("persisted"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Now().Format(time.RFC3339Nano)),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(awsutil.NewMockIAM()),
}),
},
expRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAnotrotated"),
credential.ConstSecretAccessKey: structpb.NewStringValue("notrotated"),
},
},
},
},
},
{
name: "staticRotatedCredentialToAssumeRole",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIApersisted"),
credential.ConstSecretAccessKey: structpb.NewStringValue("persisted"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Now().Format(time.RFC3339Nano)),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(awsutil.NewMockIAM()),
2026-09-24 11:12:33 -07:00
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
awsutil.WithAssumeRoleOutput(&sts.AssumeRoleOutput{
Credentials: &stsTypes.Credentials{
AccessKeyId: aws.String("ASIAfoobar"),
SecretAccessKey: aws.String("secretkeyfoobar"),
SessionToken: aws.String("sessiontokenfoobar"),
Expiration: aws.Time(time.Now().Add(time.Hour)),
},
}),
)),
}),
},
expRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{},
},
},
},
{
name: "assumeRoleCredentialToStatic",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAnew"),
credential.ConstSecretAccessKey: structpb.NewStringValue("new"),
},
},
},
Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
expRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAnew"),
credential.ConstSecretAccessKey: structpb.NewStringValue("new"),
},
},
},
},
},
{
name: "assumeRoleCredentialToStaticRotated",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAnew"),
credential.ConstSecretAccessKey: structpb.NewStringValue("new"),
},
},
},
Persisted: &pb.HostCatalogPersisted{Secrets: &structpb.Struct{}},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS()),
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserOutput(
&iam.GetUserOutput{
User: &iamTypes.User{
Arn: aws.String("arn:aws:iam::123456789012:user/JohnDoe"),
UserId: aws.String("AIDAJQABLZS4A3QDU576Q"),
UserName: aws.String("JohnDoe"),
},
},
),
awsutil.WithCreateAccessKeyOutput(
&iam.CreateAccessKeyOutput{
AccessKey: &iamTypes.AccessKey{
AccessKeyId: aws.String("AKIArotated"),
SecretAccessKey: aws.String("rotated_secret"),
UserName: aws.String("JohnDoe"),
},
},
),
),
),
}),
},
expRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIArotated"),
credential.ConstSecretAccessKey: structpb.NewStringValue("rotated_secret"),
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &HostPlugin{
testCredStateOpts: tt.credOpts,
testCatalogStateOpts: tt.catalogOpts,
}
rsp, err := p.OnUpdateCatalog(context.Background(), tt.req)
require.NoError(t, err)
delete(rsp.GetPersisted().GetSecrets().GetFields(), credential.ConstCredsLastRotatedTime)
require.Empty(t, cmp.Diff(tt.expRsp, rsp, protocmp.Transform()))
})
}
}
2021-10-07 12:40:55 -07:00
func TestPluginOnCreateCatalogErr(t *testing.T) {
2021-09-27 16:47:51 -07:00
cases := []struct {
name string
req *pb.OnCreateCatalogRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
2021-09-27 16:47:51 -07:00
}{
{
name: "nil catalog",
req: &pb.OnCreateCatalogRequest{},
expectedErrContains: "catalog is required",
expectedErrCode: codes.InvalidArgument,
2021-09-27 16:47:51 -07:00
},
{
name: "nil attributes",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-09-27 16:47:51 -07:00
},
{
name: "error reading attributes",
2021-09-27 16:47:51 -07:00
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
2021-09-27 16:47:51 -07:00
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
2021-09-27 16:47:51 -07:00
},
{
name: "failed to build credentials config",
2021-09-27 16:47:51 -07:00
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
},
},
},
2021-09-27 16:47:51 -07:00
},
},
expectedErrContains: "Error in the secrets provided: [attributes.role_arn: conflicts with access_key_id and secret_access_key values, secrets.access_key_id: conflicts with role_arn value, secrets.secret_access_key: conflicts with role_arn value]",
expectedErrCode: codes.InvalidArgument,
2021-09-27 16:47:51 -07:00
},
{
name: "credential persisted state setup error",
2021-09-27 16:47:51 -07:00
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-09-27 16:47:51 -07:00
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
func(s *credential.AwsCredentialPersistedState) error {
2021-10-06 12:47:05 -07:00
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 12:47:05 -07:00
},
{
name: "rotation error",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 12:47:05 -07:00
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
2021-10-06 12:47:05 -07:00
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
),
),
}),
},
expectedErrContains: "rotating credentials",
expectedErrCode: codes.Unknown,
},
{
name: "catalog persisted state error",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
func(s *awsCatalogPersistedState) error {
return fmt.Errorf("oops there was an error")
},
},
expectedErrContains: "error setting up persisted state: oops there was an error",
expectedErrCode: codes.InvalidArgument,
2021-10-06 12:47:05 -07:00
},
{
name: "validation error",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
2021-10-06 12:47:05 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
2021-10-06 12:47:05 -07:00
},
// checkHosts returns the DescribeInstances error as-is rather than
// wrapping it in a gRPC FailedPrecondition status like the old
// dryRunValidation did. A plain Go error maps to codes.Unknown.
expectedErrContains: testDescribeInstancesError,
expectedErrCode: codes.Unknown,
2021-09-27 16:47:51 -07:00
},
{
name: "dry run status error preserves gRPC code",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(awserrors.TestAwsError("AccessDenied", "not authorized to perform ec2:DescribeInstances")),
)),
},
// checkHosts wraps the AWS error via ParseAWSError into a gRPC
// PermissionDenied status. OnCreateCatalog's dry-run error handler
// should preserve that status code, not re-wrap it as Unknown.
expectedErrContains: "invalid credentials",
expectedErrCode: codes.PermissionDenied,
},
2026-09-24 11:12:33 -07:00
{
// BUG FIX: role_arn set, no static keys. AssumeRole mock returns
// an error. ValidateCreds now eagerly calls Retrieve() which forces
// sts:AssumeRole, so the error surfaces here at creation time.
// Previously this succeeded silently via the ambient credential chain.
name: "role_arn AssumeRole error — correctly rejected",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: catalogAttrsStruct("us-east-1", testRoleARN, true),
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
awsutil.WithAssumeRoleError(errors.New("simulated AssumeRole failure")),
)),
}),
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
expectedErrContains: "simulated AssumeRole failure",
expectedErrCode: codes.Unknown,
},
2021-09-27 16:47:51 -07:00
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &HostPlugin{
testCredStateOpts: tc.credOpts,
testCatalogStateOpts: tc.catalogOpts,
2021-10-06 12:47:05 -07:00
}
2021-09-27 16:47:51 -07:00
_, err := p.OnCreateCatalog(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
2021-09-27 16:47:51 -07:00
})
}
}
2021-10-07 12:40:55 -07:00
func TestPluginOnUpdateCatalogErr(t *testing.T) {
2021-10-06 12:47:05 -07:00
cases := []struct {
name string
req *pb.OnUpdateCatalogRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
2021-10-06 12:47:05 -07:00
}{
{
name: "nil current catalog",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: nil,
NewCatalog: &hostcatalogs.HostCatalog{},
Persisted: &pb.HostCatalogPersisted{},
},
expectedErrContains: "current catalog is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil new catalog",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{},
NewCatalog: nil,
Persisted: &pb.HostCatalogPersisted{},
},
expectedErrContains: "new catalog is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil current attributes",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: nil,
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{},
},
},
expectedErrContains: "current catalog attributes are required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading current catalog attributes",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{},
},
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
2021-10-06 12:47:05 -07:00
},
{
name: "nil new attributes",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
2021-10-06 12:47:05 -07:00
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: nil,
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{},
2021-10-06 12:47:05 -07:00
},
},
expectedErrContains: "new catalog attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 12:47:05 -07:00
},
{
name: "error reading new catalog attributes",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
2021-10-06 12:47:05 -07:00
{
name: "credential persisted state setup error",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
func(s *credential.AwsCredentialPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
},
{
name: "get new credentials config error",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
expectedErrContains: "Error in the secrets provided: [attributes.role_arn: conflicts with access_key_id and secret_access_key values, secrets.access_key_id: conflicts with role_arn value, secrets.secret_access_key: conflicts with role_arn value]",
expectedErrCode: codes.InvalidArgument,
},
{
name: "new incoming static credential update dry run error",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
//checkHosts returns the DesribeInstances errror as-is rather than wrapping it
// in a gRpc FailedPrecodndition status like the old dryRunValidiation did.
// A plain Go error maps to codeds.Unknown.
expectedErrContains: testDescribeInstancesError,
expectedErrCode: codes.Unknown,
},
{
name: "new incoming dynamic credential update dry run error",
2021-10-06 12:47:05 -07:00
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
},
},
},
},
2021-10-06 12:47:05 -07:00
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstRoleArn: structpb.NewStringValue("arn:0123:test:rolearn"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
2021-10-06 12:47:05 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 12:47:05 -07:00
},
},
2026-09-24 11:12:33 -07:00
// ValidateCreds is called for DynamicAWS credentials before the dry-run
// DescribeInstances. Provide a mock STS that returns a successful AssumeRole
// so the validate step passes and execution reaches the EC2 mock.
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithSTSAPIFunc(awsutil.NewMockSTS(
awsutil.WithAssumeRoleOutput(validAssumeRoleOutput()),
)),
}),
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
2021-10-06 12:47:05 -07:00
},
// checkHosts returns the DescribeInstances error as-is rather than wrapping it
// in a gRPC FailedPrecondition status like the old dryRunValidiation did.
// A plain Go error maps to codes.Unknown.
expectedErrContains: testDescribeInstancesError,
expectedErrCode: codes.Unknown,
2021-10-06 12:47:05 -07:00
},
{
name: "replace creds error",
2021-10-06 12:47:05 -07:00
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-east-1"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
2021-10-06 12:47:05 -07:00
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIAfoo"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bar"),
},
},
2021-10-06 12:47:05 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 12:47:05 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
)),
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(fmt.Errorf("oops delete fail")),
),
),
}),
},
expectedErrContains: "deleting credentials",
expectedErrCode: codes.Unknown,
2021-10-06 12:47:05 -07:00
},
{
name: "cannot disable rotation for already rotated credentials",
2021-10-06 12:47:05 -07:00
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 12:47:05 -07:00
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
2021-10-06 12:47:05 -07:00
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 12:47:05 -07:00
},
},
expectedErrContains: "cannot disable rotation for already-rotated credentials",
expectedErrCode: codes.InvalidArgument,
2021-10-06 12:47:05 -07:00
},
{
name: "rotation error",
2021-10-06 12:47:05 -07:00
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
2021-10-06 12:47:05 -07:00
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
2021-10-06 12:47:05 -07:00
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
2021-10-06 12:47:05 -07:00
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
2021-10-06 12:47:05 -07:00
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithGetUserError(errors.New(testGetUserErr)),
2021-10-06 12:47:05 -07:00
),
),
}),
},
expectedErrContains: "rotating credentials",
expectedErrCode: codes.Unknown,
2021-10-06 12:47:05 -07:00
},
{
name: "final dry run fail",
2021-10-06 12:47:05 -07:00
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
2021-10-06 12:47:05 -07:00
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 12:47:05 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 12:47:05 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(fmt.Errorf("oops there was an error")),
)),
2021-10-06 12:47:05 -07:00
},
// checkHosts returns the DescribeInstances error as-is rather than wrapping it
// in a gRPC FailedPrecondition status like the old dryRunValidiation did.
// A plain Go error maps to codes.Unknown.
expectedErrContains: "oops there was an error",
expectedErrCode: codes.Unknown,
2021-10-06 12:47:05 -07:00
},
{
name: "final dry run gRPC status error preserves code",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
NewCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(awserrors.TestAwsError("AccessDenied", "not authorized to perform ec2:DescribeInstances")),
)),
},
// ParseAWSError maps AccessDenied to PermissionDenied. The dry-run
// error handler should preserve that gRPC status, not wrap it as Unknown.
expectedErrContains: "invalid credentials",
expectedErrCode: codes.PermissionDenied,
},
2021-09-27 16:47:51 -07:00
}
2021-10-06 12:47:05 -07:00
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &HostPlugin{
testCredStateOpts: tc.credOpts,
testCatalogStateOpts: tc.catalogOpts,
2021-10-06 12:47:05 -07:00
}
_, err := p.OnUpdateCatalog(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
2021-10-06 12:47:05 -07:00
})
}
2021-09-27 16:47:51 -07:00
}
2021-10-06 16:53:11 -07:00
2021-10-07 12:40:55 -07:00
func TestPluginOnDeleteCatalogErr(t *testing.T) {
2021-10-06 16:53:11 -07:00
cases := []struct {
name string
req *pb.OnDeleteCatalogRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
2021-10-06 16:53:11 -07:00
}{
{
name: "persisted state setup error",
req: &pb.OnDeleteCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
2021-10-06 16:53:11 -07:00
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "delete error",
req: &pb.OnDeleteCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
2021-10-06 16:53:11 -07:00
awsutil.WithIAMAPIFunc(
awsutil.NewMockIAM(
awsutil.WithDeleteAccessKeyError(errors.New(testDeleteAccessKeyErr)),
),
),
}),
},
expectedErrContains: "aws service unknown: unknown error: deleting credentials:",
expectedErrCode: codes.Unknown,
2021-10-06 16:53:11 -07:00
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &HostPlugin{
testCredStateOpts: tc.credOpts,
testCatalogStateOpts: tc.catalogOpts,
2021-10-06 16:53:11 -07:00
}
_, err := p.OnDeleteCatalog(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
2021-10-06 16:53:11 -07:00
})
}
}
2021-10-07 12:40:55 -07:00
func TestPluginOnCreateSetErr(t *testing.T) {
2021-10-06 16:53:11 -07:00
cases := []struct {
name string
req *pb.OnCreateSetRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
2021-10-06 16:53:11 -07:00
}{
{
name: "nil catalog",
req: &pb.OnCreateSetRequest{},
expectedErrContains: "catalog is required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil catalog attributes",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "catalog attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "error reading catalog attributes",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
2021-10-06 16:53:11 -07:00
{
name: "persisted state setup error",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
2021-10-06 16:53:11 -07:00
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil set",
2021-10-06 16:53:11 -07:00
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
expectedErrContains: "set is required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil attributes in set",
2021-10-06 16:53:11 -07:00
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Set: &hostsets.HostSet{},
2021-10-06 16:53:11 -07:00
},
expectedErrContains: "set attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "set attribute load error",
2021-10-06 16:53:11 -07:00
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
},
},
2021-10-06 16:53:11 -07:00
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "client load error",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
2021-10-06 16:53:11 -07:00
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "DescribeInstances error",
2021-10-06 16:53:11 -07:00
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
expectedErrContains: testDescribeInstancesError,
expectedErrCode: codes.Unknown,
2021-10-06 16:53:11 -07:00
},
{
name: "dry run gRPC status error preserves code",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
},
Set: &hostsets.HostSet{
Id: "foobar",
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(awserrors.TestAwsError("AccessDenied", "not authorized to perform ec2:DescribeInstances")),
)),
},
// ParseAWSError maps AccessDenied to PermissionDenied. The dry-run
// error handler should preserve that gRPC status, not wrap it as Unknown.
expectedErrContains: "invalid credentials",
expectedErrCode: codes.PermissionDenied,
},
2021-10-06 16:53:11 -07:00
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &HostPlugin{
testCredStateOpts: tc.credOpts,
testCatalogStateOpts: tc.catalogOpts,
2021-10-06 16:53:11 -07:00
}
_, err := p.OnCreateSet(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
2021-10-06 16:53:11 -07:00
})
}
}
2021-10-07 12:40:55 -07:00
func TestPluginOnUpdateSetErr(t *testing.T) {
2021-10-06 16:53:11 -07:00
cases := []struct {
name string
req *pb.OnUpdateSetRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
2021-10-06 16:53:11 -07:00
}{
{
name: "nil catalog",
req: &pb.OnUpdateSetRequest{},
expectedErrContains: "catalog is required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil catalog attributes",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "catalog attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "catalog attribute load error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
2021-10-06 16:53:11 -07:00
{
name: "persisted state setup error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
2021-10-06 16:53:11 -07:00
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil set",
2021-10-06 16:53:11 -07:00
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
expectedErrContains: "new set is required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil attributes in set",
2021-10-06 16:53:11 -07:00
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
NewSet: &hostsets.HostSet{},
2021-10-06 16:53:11 -07:00
},
expectedErrContains: "new set attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "set attribute load error",
2021-10-06 16:53:11 -07:00
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
NewSet: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
},
},
2021-10-06 16:53:11 -07:00
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "client load error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
NewSet: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
2021-10-06 16:53:11 -07:00
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "DescribeInstances error",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
NewSet: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
2021-10-06 16:53:11 -07:00
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
expectedErrContains: testDescribeInstancesError,
expectedErrCode: codes.Unknown,
2021-10-06 16:53:11 -07:00
},
{
name: "dry run gRPC status error preserves code",
req: &pb.OnUpdateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
},
NewSet: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(awserrors.TestAwsError("AccessDenied", "not authorized to perform ec2:DescribeInstances")),
)),
},
// ParseAWSError maps AccessDenied to PermissionDenied. The dry-run
// error handler should preserve that gRPC status, not wrap it as Unknown.
expectedErrContains: "invalid credentials",
expectedErrCode: codes.PermissionDenied,
},
2021-10-06 16:53:11 -07:00
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &HostPlugin{
testCredStateOpts: tc.credOpts,
testCatalogStateOpts: tc.catalogOpts,
2021-10-06 16:53:11 -07:00
}
_, err := p.OnUpdateSet(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
2021-10-06 16:53:11 -07:00
})
}
}
func testListHostsSet(id, filter string) *hostsets.HostSet {
return &hostsets.HostSet{
Id: id,
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue(filter),
},
},
),
},
},
},
}
}
func testListHostsPersisted() *pb.HostCatalogPersisted {
return &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
},
},
}
}
func testExpectedDescribeInput(filterKey, filterValue string) *ec2.DescribeInstancesInput {
return &ec2.DescribeInstancesInput{
DryRun: aws.Bool(false),
Filters: []types.Filter{
{
Name: aws.String(filterKey),
Values: []string{filterValue},
},
{
Name: aws.String("instance-state-name"),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
}
}
func TestPluginListHosts(t *testing.T) {
describeOutput := &ec2.DescribeInstancesOutput{
Reservations: []types.Reservation{
{
Instances: []types.Instance{
{
InstanceId: aws.String("i-shared"),
PrivateIpAddress: aws.String("10.0.0.1"),
},
},
},
},
}
cases := []struct {
name string
catalogFields map[string]*structpb.Value
sets []*hostsets.HostSet
expectedRegion string
expectedHosts []*pb.ListHostsResponseHost
expectedInputs []*ec2.DescribeInstancesInput
expectedDescribe int
}{
{
name: "principal account lists hosts",
catalogFields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
sets: []*hostsets.HostSet{
testListHostsSet("set-1", "tag-key=foo"),
},
expectedRegion: "us-west-2",
expectedHosts: []*pb.ListHostsResponseHost{
{
ExternalId: "i-shared",
SetIds: []string{"set-1"},
},
},
expectedInputs: []*ec2.DescribeInstancesInput{
testExpectedDescribeInput("tag-key", "foo"),
},
expectedDescribe: 1,
},
{
name: "target account lists hosts without querying principal",
catalogFields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
credential.ConstTargetRegion: structpb.NewStringValue("eu-west-1"),
},
sets: []*hostsets.HostSet{
testListHostsSet("set-1", "tag-key=foo"),
testListHostsSet("set-2", "tag-key=bar"),
},
expectedRegion: "eu-west-1",
expectedHosts: []*pb.ListHostsResponseHost{
{
ExternalId: "i-shared",
SetIds: []string{"set-1", "set-2"},
},
},
expectedInputs: []*ec2.DescribeInstancesInput{
testExpectedDescribeInput("tag-key", "foo"),
testExpectedDescribeInput("tag-key", "bar"),
},
expectedDescribe: 2,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
state := &testMockEC2State{}
p := &HostPlugin{
testCatalogStateOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
state,
testMockEC2WithDescribeInstancesOutput(describeOutput),
)),
},
}
resp, err := p.ListHosts(context.Background(), &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{Fields: tc.catalogFields},
},
},
Persisted: testListHostsPersisted(),
Sets: tc.sets,
})
require.NoError(err)
require.NotNil(resp)
require.Len(resp.GetHosts(), len(tc.expectedHosts))
for i, expected := range tc.expectedHosts {
require.Equal(expected.ExternalId, resp.Hosts[i].ExternalId)
require.Equal(expected.SetIds, resp.Hosts[i].SetIds)
}
require.Equal(tc.expectedDescribe, state.DescribeInstancesCallCount)
require.Len(state.ClientRegions, tc.expectedDescribe)
for _, region := range state.ClientRegions {
require.Equal(tc.expectedRegion, region)
}
require.Equal(tc.expectedInputs, state.DescribeInstancesInputs)
})
}
}
2021-10-07 12:40:55 -07:00
func TestPluginListHostsErr(t *testing.T) {
2021-10-06 16:53:11 -07:00
cases := []struct {
name string
req *pb.ListHostsRequest
credOpts []credential.AwsCredentialPersistedStateOption
catalogOpts []awsCatalogPersistedStateOption
expectedErrContains string
expectedErrCode codes.Code
2021-10-06 16:53:11 -07:00
}{
{
name: "nil catalog",
req: &pb.ListHostsRequest{},
expectedErrContains: "catalog is required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil catalog attributes",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErrContains: "catalog attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "error reading catalog attributes",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErrContains: "attributes.region: missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
},
2021-10-06 16:53:11 -07:00
{
name: "persisted state setup error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
catalogOpts: []awsCatalogPersistedStateOption{
2021-10-06 16:53:11 -07:00
func(s *awsCatalogPersistedState) error {
return errors.New(testOptionErr)
},
},
expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "nil sets",
2021-10-06 16:53:11 -07:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
},
expectedErrContains: "sets are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "set missing id",
2021-10-06 16:53:11 -07:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Sets: []*hostsets.HostSet{{}},
2021-10-06 16:53:11 -07:00
},
expectedErrContains: "set id is required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "set missing attributes",
2021-10-06 16:53:11 -07:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Sets: []*hostsets.HostSet{
{
Id: "foobar",
},
},
2021-10-06 16:53:11 -07:00
},
expectedErrContains: "set foobar attributes are required",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "set attribute load error",
2021-10-06 16:53:11 -07:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Sets: []*hostsets.HostSet{
{
2021-10-06 16:53:11 -07:00
Id: "foobar",
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
},
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "client load error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Sets: []*hostsets.HostSet{
{
2021-10-06 16:53:11 -07:00
Id: "foobar",
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
},
credOpts: []credential.AwsCredentialPersistedStateOption{
credential.WithStateTestOpts([]awsutil.Option{
2021-10-06 16:53:11 -07:00
awsutil.MockOptionErr(errors.New(testOptionErr)),
}),
},
expectedErrContains: fmt.Sprintf("error loading persisted state: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
2021-10-06 16:53:11 -07:00
},
{
name: "DescribeInstances error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Sets: []*hostsets.HostSet{
{
2021-10-06 16:53:11 -07:00
Id: "foobar",
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
2021-10-06 16:53:11 -07:00
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
expectedErrContains: fmt.Sprintf("error retrieving host results for host set id \"foobar\": %s", testDescribeInstancesError),
expectedErrCode: codes.Unknown,
},
{
name: "target attributes missing target_role_arn",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstTargetRegion: structpb.NewStringValue("eu-west-1"),
},
},
},
},
},
expectedErrContains: "attributes.target_role_arn: missing required value \"target_role_arn\"",
expectedErrCode: codes.InvalidArgument,
},
{
name: "target EC2 client error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
Sets: []*hostsets.HostSet{
{
Id: "foobar",
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(func(...aws.Config) (EC2API, error) {
return nil, errors.New("test EC2 client error")
}),
},
expectedErrContains: "unable to list hosts for host set \"foobar\": EC2 client setup failed for target account: test EC2 client error",
expectedErrCode: codes.Unknown,
},
{
name: "target DescribeInstances error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
credential.ConstTargetRegion: structpb.NewStringValue("eu-west-1"),
},
},
},
},
Persisted: testListHostsPersisted(),
Sets: []*hostsets.HostSet{
testListHostsSet("foobar", "tag-key=foo"),
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)),
},
expectedErrContains: "unable to list hosts for host set \"foobar\": EC2 DescribeInstances failed for target account: DescribeInstances error",
expectedErrCode: codes.Unknown,
2021-10-06 16:53:11 -07:00
},
{
name: "awsInstanceToHost error",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
2021-10-06 16:53:11 -07:00
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstAccessKeyId: structpb.NewStringValue("AKIA_foobar"),
credential.ConstSecretAccessKey: structpb.NewStringValue("bazqux"),
credential.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
2021-10-06 16:53:11 -07:00
},
Sets: []*hostsets.HostSet{
{
2021-10-06 16:53:11 -07:00
Id: "foobar",
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstDescribeInstancesFilters: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
2021-10-06 16:53:11 -07:00
},
},
},
catalogOpts: []awsCatalogPersistedStateOption{
2021-10-06 16:53:11 -07:00
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesOutput(
&ec2.DescribeInstancesOutput{
Reservations: []types.Reservation{
{
Instances: []types.Instance{
{
2021-10-06 16:53:11 -07:00
// Blank so we error out
},
},
},
},
},
),
)),
},
expectedErrContains: "error processing host results for host set id \"foobar\": response integrity error: missing instance id",
expectedErrCode: codes.Unknown,
},
{
name: "DescribeInstances AccessDenied",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
Persisted: testListHostsPersisted(),
Sets: []*hostsets.HostSet{
testListHostsSet("foobar", "tag-key=foo"),
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(awserrors.TestAwsError("AccessDenied", "not authorized to perform ec2:DescribeInstances")),
)),
},
expectedErrContains: "invalid credentials",
expectedErrCode: codes.PermissionDenied,
},
{
name: "DescribeInstances UnauthorizedOperation",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
},
},
},
},
Persisted: testListHostsPersisted(),
Sets: []*hostsets.HostSet{
testListHostsSet("foobar", "tag-key=foo"),
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(awserrors.TestAwsError("UnauthorizedOperation", "You are not authorized to perform this operation.")),
)),
},
expectedErrContains: "invalid credentials",
expectedErrCode: codes.PermissionDenied,
},
{
name: "target DescribeInstances AccessDenied",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
credential.ConstTargetRegion: structpb.NewStringValue("eu-west-1"),
},
},
},
},
Persisted: testListHostsPersisted(),
Sets: []*hostsets.HostSet{
testListHostsSet("foobar", "tag-key=foo"),
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(newTestMockEC2(
nil,
testMockEC2WithDescribeInstancesError(awserrors.TestAwsError("AccessDenied", "not authorized to perform ec2:DescribeInstances")),
)),
},
expectedErrContains: "invalid credentials",
expectedErrCode: codes.PermissionDenied,
},
{
name: "target EC2 client AccessDenied",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
credential.ConstRegion: structpb.NewStringValue("us-west-2"),
credential.ConstTargetRoleArn: structpb.NewStringValue("arn:aws:iam::222222222222:role/Target"),
},
},
},
},
Persisted: testListHostsPersisted(),
Sets: []*hostsets.HostSet{
testListHostsSet("foobar", "tag-key=foo"),
},
},
catalogOpts: []awsCatalogPersistedStateOption{
withTestEC2APIFunc(func(...aws.Config) (EC2API, error) {
return nil, awserrors.TestAwsError("AccessDenied", "not authorized to assume role")
}),
},
// ec2ClientForTarget returns testEC2APIFunc errors unparsed; checkHosts
// only preserves existing gRPC statuses, so this surfaces as Unknown.
expectedErrContains: "unable to list hosts for host set \"foobar\": EC2 client setup failed for target account: api error AccessDenied: not authorized to assume role",
expectedErrCode: codes.Unknown,
2021-10-06 16:53:11 -07:00
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &HostPlugin{
testCredStateOpts: tc.credOpts,
testCatalogStateOpts: tc.catalogOpts,
2021-10-06 16:53:11 -07:00
}
_, err := p.ListHosts(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
2021-10-06 16:53:11 -07:00
})
}
}
2021-10-07 12:40:55 -07:00
func TestBuildFilters(t *testing.T) {
cases := []struct {
name string
in map[string]any
expected []types.Filter
expectedErrContains string
2021-10-07 12:40:55 -07:00
}{
{
name: "good without instance-state-name",
in: map[string]any{
ConstDescribeInstancesFilters: []any{"foo=bar"},
},
expected: []types.Filter{
{
Name: aws.String("foo"),
Values: []string{"bar"},
2021-10-07 12:40:55 -07:00
},
{
Name: aws.String("instance-state-name"),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
2021-10-07 12:40:55 -07:00
},
{
name: "good with instance-state-name",
in: map[string]any{
ConstDescribeInstancesFilters: []any{
"foo=bar",
"instance-state-name=static",
2021-10-07 12:40:55 -07:00
},
},
expected: []types.Filter{
{
2021-10-07 12:40:55 -07:00
Name: aws.String("foo"),
Values: []string{"bar"},
2021-10-07 12:40:55 -07:00
},
{
2021-10-07 12:40:55 -07:00
Name: aws.String("instance-state-name"),
Values: []string{"static"},
2021-10-07 12:40:55 -07:00
},
},
},
{
name: "good with multiple values",
in: map[string]any{
ConstDescribeInstancesFilters: []any{
"foo=bar,baz",
2021-10-07 12:40:55 -07:00
},
},
expected: []types.Filter{
{
2021-10-07 12:40:55 -07:00
Name: aws.String("foo"),
Values: []string{"bar", "baz"},
2021-10-07 12:40:55 -07:00
},
{
2021-10-07 12:40:55 -07:00
Name: aws.String("instance-state-name"),
Values: []string{string(types.InstanceStateNameRunning)},
2021-10-07 12:40:55 -07:00
},
},
},
{
name: "empty filter set",
in: map[string]any{
ConstDescribeInstancesFilters: []any{},
},
expected: []types.Filter{
{
2021-10-07 12:40:55 -07:00
Name: aws.String("instance-state-name"),
Values: []string{string(types.InstanceStateNameRunning)},
2021-10-07 12:40:55 -07:00
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
attrs, err := getSetAttributes(input)
require.NoError(err)
actual, err := buildFilters(attrs)
if tc.expectedErrContains != "" {
require.Contains(err.Error(), tc.expectedErrContains)
2021-10-07 12:40:55 -07:00
return
}
require.NoError(err)
sort.Sort(ec2FilterSorter(actual))
require.Equal(tc.expected, actual)
})
}
}
func TestBuildDescribeInstancesInput(t *testing.T) {
cases := []struct {
name string
in map[string]any
2021-10-07 12:40:55 -07:00
dryRun bool
expected *ec2.DescribeInstancesInput
expectedErr string
}{
{
name: "good, dry run",
in: map[string]any{
ConstDescribeInstancesFilters: []any{
"foo=bar",
2021-10-07 12:40:55 -07:00
},
},
2021-10-07 12:40:55 -07:00
dryRun: true,
expected: &ec2.DescribeInstancesInput{
DryRun: aws.Bool(true),
Filters: []types.Filter{
{
2021-10-07 12:40:55 -07:00
Name: aws.String("foo"),
Values: []string{"bar"},
2021-10-07 12:40:55 -07:00
},
{
2021-10-07 12:40:55 -07:00
Name: aws.String("instance-state-name"),
Values: []string{string(types.InstanceStateNameRunning)},
2021-10-07 12:40:55 -07:00
},
},
},
},
{
name: "good, real run",
in: map[string]any{
ConstDescribeInstancesFilters: []any{
"foo=bar",
2021-10-07 12:40:55 -07:00
},
},
2021-10-07 12:40:55 -07:00
dryRun: false,
expected: &ec2.DescribeInstancesInput{
DryRun: aws.Bool(false),
Filters: []types.Filter{
{
2021-10-07 12:40:55 -07:00
Name: aws.String("foo"),
Values: []string{"bar"},
2021-10-07 12:40:55 -07:00
},
{
2021-10-07 12:40:55 -07:00
Name: aws.String("instance-state-name"),
Values: []string{string(types.InstanceStateNameRunning)},
2021-10-07 12:40:55 -07:00
},
},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
input, err := structpb.NewStruct(tc.in)
require.NoError(err)
attrs, err := getSetAttributes(input)
require.NoError(err)
actual, err := buildDescribeInstancesInput(attrs, tc.dryRun)
2021-10-07 12:40:55 -07:00
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
t.Run("nil attrs, dry run", func(t *testing.T) {
require := require.New(t)
actual, err := buildDescribeInstancesInput(nil, true)
require.NoError(err)
require.Equal(&ec2.DescribeInstancesInput{
DryRun: aws.Bool(true),
Filters: []types.Filter{
{
Name: aws.String("instance-state-name"),
Values: []string{string(types.InstanceStateNameRunning)},
},
},
}, actual)
})
2021-10-07 12:40:55 -07:00
}
func TestAwsInstanceToHost(t *testing.T) {
cases := []struct {
name string
instance types.Instance
catalogAttrs *CatalogAttributes
expected *pb.ListHostsResponseHost
expectedErr string
2021-10-07 12:40:55 -07:00
}{
{
name: "missing instance id",
instance: types.Instance{},
catalogAttrs: &CatalogAttributes{},
expectedErr: "response integrity error: missing instance id",
},
{
name: "missing catalog attributes",
instance: types.Instance{
InstanceId: aws.String("foobar"),
},
catalogAttrs: nil,
expectedErr: "response integrity error: missing catalog attributes",
2021-10-07 12:40:55 -07:00
},
{
name: "good, single IP w/public addr",
instance: types.Instance{
2021-10-07 12:40:55 -07:00
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
PublicIpAddress: aws.String("1.1.1.1"),
2021-12-03 13:24:48 -08:00
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
2021-12-03 13:24:48 -08:00
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
2021-10-07 12:40:55 -07:00
},
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
},
},
},
},
},
catalogAttrs: &CatalogAttributes{},
2021-10-07 12:40:55 -07:00
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
2021-12-03 13:24:48 -08:00
DnsNames: []string{"test.example.internal", "test.example.com"},
2021-10-07 12:40:55 -07:00
},
},
{
name: "good, private",
instance: types.Instance{
2021-10-07 12:40:55 -07:00
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
2021-10-07 12:40:55 -07:00
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
},
},
},
},
},
catalogAttrs: &CatalogAttributes{},
2021-10-07 12:40:55 -07:00
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1"},
2021-12-03 13:24:48 -08:00
DnsNames: []string{"test.example.internal"},
2021-10-07 12:40:55 -07:00
},
},
{
name: "good, multiple interfaces",
instance: types.Instance{
2021-10-07 12:40:55 -07:00
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
PublicIpAddress: aws.String("1.1.1.1"),
2021-12-03 13:24:48 -08:00
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("test2.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
2021-10-07 12:40:55 -07:00
PrivateIpAddress: aws.String("10.0.0.2"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test2.example.internal"),
2021-10-07 12:40:55 -07:00
},
},
},
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
2021-12-03 13:24:48 -08:00
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
2021-10-07 12:40:55 -07:00
},
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
},
},
},
},
},
catalogAttrs: &CatalogAttributes{},
2021-10-07 12:40:55 -07:00
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.2"},
2021-12-03 13:24:48 -08:00
DnsNames: []string{"test.example.internal", "test.example.com", "test2.example.internal"},
2021-10-07 12:40:55 -07:00
},
},
{
name: "good, multiple public interfaces",
instance: types.Instance{
2021-10-07 12:40:55 -07:00
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
PublicIpAddress: aws.String("1.1.1.1"),
2021-12-03 13:24:48 -08:00
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("test2.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
2021-12-03 13:24:48 -08:00
PublicIp: aws.String("1.1.1.2"),
PublicDnsName: aws.String("test2.example.com"),
2021-10-07 12:40:55 -07:00
},
PrivateIpAddress: aws.String("10.0.0.2"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test2.example.internal"),
2021-10-07 12:40:55 -07:00
},
},
},
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
2021-12-03 13:24:48 -08:00
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
2021-10-07 12:40:55 -07:00
},
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
},
},
},
},
},
catalogAttrs: &CatalogAttributes{},
2021-10-07 12:40:55 -07:00
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.2", "1.1.1.2"},
2021-12-03 13:24:48 -08:00
DnsNames: []string{"test.example.internal", "test.example.com", "test2.example.internal", "test2.example.com"},
2021-10-07 12:40:55 -07:00
},
},
{
name: "good, multiple addresses on single interface",
instance: types.Instance{
2021-10-07 12:40:55 -07:00
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
PublicIpAddress: aws.String("1.1.1.1"),
2021-12-03 13:24:48 -08:00
PublicDnsName: aws.String("test.example.com"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
2021-12-03 13:24:48 -08:00
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
2021-10-07 12:40:55 -07:00
},
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
},
{
2021-10-07 12:40:55 -07:00
PrivateIpAddress: aws.String("10.0.0.2"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test2.example.internal"),
2021-10-07 12:40:55 -07:00
},
},
},
},
},
catalogAttrs: &CatalogAttributes{},
2021-10-07 12:40:55 -07:00
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "10.0.0.2"},
2021-12-03 13:24:48 -08:00
DnsNames: []string{"test.example.internal", "test.example.com", "test2.example.internal"},
2021-10-07 12:40:55 -07:00
},
},
{
name: "good, single IP w/public addr and IPv6",
instance: types.Instance{
2021-10-07 12:40:55 -07:00
InstanceId: aws.String("foobar"),
PrivateIpAddress: aws.String("10.0.0.1"),
2021-12-03 13:24:48 -08:00
PrivateDnsName: aws.String("test.example.internal"),
2021-10-07 12:40:55 -07:00
PublicIpAddress: aws.String("1.1.1.1"),
2021-12-03 13:24:48 -08:00
PublicDnsName: aws.String("test.example.com"),
Ipv6Address: aws.String("instance::ipv6::address"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
2021-10-07 12:40:55 -07:00
PublicIp: aws.String("1.1.1.1"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{
{Ipv6Address: nil}, // Just coverage for nil assertion which is skipped
{Ipv6Address: aws.String("some::fake::address")},
2021-10-07 12:40:55 -07:00
},
},
},
},
catalogAttrs: &CatalogAttributes{},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "instance::ipv6::address", "some::fake::address"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "good, instance addresses only excludes interface secondary addresses",
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"),
Ipv6Address: aws.String("instance::ipv6::address"),
NetworkInterfaces: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
},
{
PrivateIpAddress: aws.String("10.0.0.2"),
PrivateDnsName: aws.String("test2.example.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("iface::ipv6::address")}},
},
{
PrivateIpAddress: aws.String("10.0.1.1"),
PrivateDnsName: aws.String("secondary.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("2.2.2.2"),
PublicDnsName: aws.String("secondary.example.com"),
},
PrivateIpAddress: aws.String("10.0.1.1"),
PrivateDnsName: aws.String("secondary.example.internal"),
},
},
Ipv6Addresses: []types.InstanceIpv6Address{{Ipv6Address: aws.String("secondary::ipv6::address")}},
},
},
},
catalogAttrs: &CatalogAttributes{InstanceAddressesOnly: true},
2021-10-07 12:40:55 -07:00
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
IpAddresses: []string{"10.0.0.1", "1.1.1.1", "instance::ipv6::address"},
2021-12-03 13:24:48 -08:00
DnsNames: []string{"test.example.internal", "test.example.com"},
2021-10-07 12:40:55 -07:00
},
},
{
name: "good, single IP w/public addr and external name",
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: []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: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
},
},
},
},
},
catalogAttrs: &CatalogAttributes{},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
ExternalName: "test-instance-actual-name",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
{
name: "good, single IP w/public addr and external name",
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: []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: []types.InstanceNetworkInterface{
{
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
PrivateIpAddresses: []types.InstancePrivateIpAddress{
{
Association: &types.InstanceNetworkInterfaceAssociation{
PublicIp: aws.String("1.1.1.1"),
PublicDnsName: aws.String("test.example.com"),
},
PrivateIpAddress: aws.String("10.0.0.1"),
PrivateDnsName: aws.String("test.example.internal"),
},
},
},
},
},
catalogAttrs: &CatalogAttributes{},
expected: &pb.ListHostsResponseHost{
ExternalId: "foobar",
ExternalName: "test-instance-actual-name",
IpAddresses: []string{"10.0.0.1", "1.1.1.1"},
DnsNames: []string{"test.example.internal", "test.example.com"},
},
},
2021-10-07 12:40:55 -07:00
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := awsInstanceToHost(tc.instance, tc.catalogAttrs)
2021-10-07 12:40:55 -07:00
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
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)
})
}
}