Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f81151b58c |
No files matched your search
@@ -23,6 +23,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// AwsErrorAuthFailure is returned by EC2 when credentials exist in IAM
|
||||||
|
// but have not yet propagated to EC2. This is a transient eventual
|
||||||
|
// consistency issue that can resolve itself by retrying the request.
|
||||||
|
AwsErrorAuthFailure = "AuthFailure"
|
||||||
|
|
||||||
// awsErrorAccessDenied is returned when the credentials do not have
|
// awsErrorAccessDenied is returned when the credentials do not have
|
||||||
// the required permissions to do a specific action on a resource.
|
// the required permissions to do a specific action on a resource.
|
||||||
// This error will persist until the IAM Role Policy is updated by
|
// This error will persist until the IAM Role Policy is updated by
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
goerrors "errors"
|
goerrors "errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
|
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
|
||||||
@@ -828,10 +829,18 @@ func ec2ClientForTarget(
|
|||||||
return ec2.NewFromConfig(targetCfg, ec2Opts...), nil
|
return ec2.NewFromConfig(targetCfg, ec2Opts...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dryRunValidationTimeout is the maximum time to wait for credentials to
|
||||||
|
// propagate across AWS services after rotation before giving up.
|
||||||
|
const dryRunValidationTimeout = 30 * time.Second
|
||||||
|
|
||||||
// dryRunValidation performs an AWS DescribeInstances call to verify the state's
|
// dryRunValidation performs an AWS DescribeInstances call to verify the state's
|
||||||
// credentials, the host listing functionality as well as the filters, if any
|
// credentials, the host listing functionality as well as the filters, if any
|
||||||
// are passed in. This function can therefore be used for both host catalog and
|
// are passed in. This function can therefore be used for both host catalog and
|
||||||
// host set validation.
|
// host set validation.
|
||||||
|
//
|
||||||
|
// After credential rotation, IAM keys may be valid for STS (GetCallerIdentity)
|
||||||
|
// but not yet propagated to EC2. This function retries on AuthFailure to
|
||||||
|
// tolerate that eventual consistency window.
|
||||||
func dryRunValidation(ctx context.Context, state *awsCatalogPersistedState, ec2Opts []ec2Option, filters ...types.Filter) *status.Status {
|
func dryRunValidation(ctx context.Context, state *awsCatalogPersistedState, ec2Opts []ec2Option, filters ...types.Filter) *status.Status {
|
||||||
if state == nil {
|
if state == nil {
|
||||||
return status.New(codes.InvalidArgument, "persisted state is required")
|
return status.New(codes.InvalidArgument, "persisted state is required")
|
||||||
@@ -842,12 +851,34 @@ func dryRunValidation(ctx context.Context, state *awsCatalogPersistedState, ec2O
|
|||||||
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting EC2 client: %s", err))
|
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting EC2 client: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{Filters: filters})
|
deadline := time.Now().Add(dryRunValidationTimeout)
|
||||||
if err != nil {
|
delay := time.Second
|
||||||
return status.New(codes.FailedPrecondition, fmt.Sprintf("aws describe instances failed: %s", err))
|
for {
|
||||||
}
|
_, err = ec2Client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{Filters: filters})
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
// AuthFailure indicates the credentials exist but haven't propagated
|
||||||
|
// to EC2 yet — a transient eventual consistency issue after rotation.
|
||||||
|
// Retry until the deadline.
|
||||||
|
if strings.Contains(err.Error(), errors.AwsErrorAuthFailure) && time.Now().Before(deadline) {
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
// Wait before retrying to give credentials time to propagate.
|
||||||
|
case <-ctx.Done():
|
||||||
|
// The caller cancelled — break out of the select.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return status.New(codes.FailedPrecondition, fmt.Sprintf("aws describe instances failed: %s", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendDistinct will append the elements to the slice
|
// appendDistinct will append the elements to the slice
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
|
pluginErrors "github.com/hashicorp/boundary-plugin-aws/internal/errors"
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
@@ -3121,4 +3122,66 @@ func TestDryRunValidation(t *testing.T) {
|
|||||||
}, []ec2Option{})
|
}, []ec2Option{})
|
||||||
require.Nil(t, st)
|
require.Nil(t, st)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("authFailureThenSuccess", func(t *testing.T) {
|
||||||
|
// Simulates the IAM eventual consistency window: EC2 returns AuthFailure
|
||||||
|
// twice before the credentials propagate and the call succeeds.
|
||||||
|
state := &testMockEC2State{}
|
||||||
|
st := dryRunValidation(context.Background(), &awsCatalogPersistedState{
|
||||||
|
AwsCredentialPersistedState: &credential.AwsCredentialPersistedState{
|
||||||
|
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||||
|
AccessKey: "AKIAfoo",
|
||||||
|
SecretKey: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testEC2APIFunc: newTestMockEC2(state,
|
||||||
|
testMockEC2WithDescribeInstancesErrorSequence(
|
||||||
|
fmt.Errorf("%s: credentials not yet propagated", pluginErrors.AwsErrorAuthFailure),
|
||||||
|
fmt.Errorf("%s: credentials not yet propagated", pluginErrors.AwsErrorAuthFailure),
|
||||||
|
),
|
||||||
|
testMockEC2WithDescribeInstancesOutput(&ec2.DescribeInstancesOutput{}),
|
||||||
|
),
|
||||||
|
}, []ec2Option{})
|
||||||
|
require.Nil(t, st)
|
||||||
|
require.Equal(t, 3, state.DescribeInstancesCallCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("authFailureContextCancelled", func(t *testing.T) {
|
||||||
|
// If the context is cancelled while waiting to retry, the function should
|
||||||
|
// return a FailedPrecondition error immediately rather than hanging.
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel() // cancel immediately so the first retry select hits ctx.Done()
|
||||||
|
st := dryRunValidation(ctx, &awsCatalogPersistedState{
|
||||||
|
AwsCredentialPersistedState: &credential.AwsCredentialPersistedState{
|
||||||
|
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||||
|
AccessKey: "AKIAfoo",
|
||||||
|
SecretKey: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testEC2APIFunc: newTestMockEC2(nil,
|
||||||
|
testMockEC2WithDescribeInstancesError(fmt.Errorf("%s: credentials not yet propagated", pluginErrors.AwsErrorAuthFailure)),
|
||||||
|
),
|
||||||
|
}, []ec2Option{})
|
||||||
|
require.NotNil(t, st)
|
||||||
|
require.Equal(t, codes.FailedPrecondition.String(), st.Code().String())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nonAuthFailureNoRetry", func(t *testing.T) {
|
||||||
|
// A non-AuthFailure error should fail immediately without retrying.
|
||||||
|
state := &testMockEC2State{}
|
||||||
|
st := dryRunValidation(context.Background(), &awsCatalogPersistedState{
|
||||||
|
AwsCredentialPersistedState: &credential.AwsCredentialPersistedState{
|
||||||
|
CredentialsConfig: &awsutil.CredentialsConfig{
|
||||||
|
AccessKey: "AKIAfoo",
|
||||||
|
SecretKey: "baz",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testEC2APIFunc: newTestMockEC2(state,
|
||||||
|
testMockEC2WithDescribeInstancesError(fmt.Errorf("UnauthorizedOperation: access denied")),
|
||||||
|
),
|
||||||
|
}, []ec2Option{})
|
||||||
|
require.NotNil(t, st)
|
||||||
|
require.Equal(t, codes.FailedPrecondition.String(), st.Code().String())
|
||||||
|
require.Equal(t, 1, state.DescribeInstancesCallCount)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -43,6 +43,10 @@ type testMockEC2 struct {
|
|||||||
|
|
||||||
DescribeInstancesOutput *ec2.DescribeInstancesOutput
|
DescribeInstancesOutput *ec2.DescribeInstancesOutput
|
||||||
DescribeInstancesError error
|
DescribeInstancesError error
|
||||||
|
|
||||||
|
// DescribeInstancesErrorSequence, if set, returns each error in order on
|
||||||
|
// successive calls. Once exhausted, DescribeInstancesOutput is returned.
|
||||||
|
DescribeInstancesErrorSequence []error
|
||||||
}
|
}
|
||||||
|
|
||||||
type testMockEC2Option func(m *testMockEC2) error
|
type testMockEC2Option func(m *testMockEC2) error
|
||||||
@@ -61,6 +65,16 @@ func testMockEC2WithDescribeInstancesError(e error) testMockEC2Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testMockEC2WithDescribeInstancesErrorSequence sets a sequence of errors to
|
||||||
|
// return on successive DescribeInstances calls. Once the sequence is exhausted,
|
||||||
|
// the mock returns DescribeInstancesOutput (nil error).
|
||||||
|
func testMockEC2WithDescribeInstancesErrorSequence(errs ...error) testMockEC2Option {
|
||||||
|
return func(m *testMockEC2) error {
|
||||||
|
m.DescribeInstancesErrorSequence = errs
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc {
|
func newTestMockEC2(state *testMockEC2State, opts ...testMockEC2Option) ec2APIFunc {
|
||||||
return func(cfgs ...aws.Config) (EC2API, error) {
|
return func(cfgs ...aws.Config) (EC2API, error) {
|
||||||
m := &testMockEC2{
|
m := &testMockEC2{
|
||||||
@@ -96,6 +110,12 @@ func (m *testMockEC2) DescribeInstances(ctx context.Context, input *ec2.Describe
|
|||||||
m.State.DescribeInstancesInputs = append(m.State.DescribeInstancesInputs, input)
|
m.State.DescribeInstancesInputs = append(m.State.DescribeInstancesInputs, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(m.DescribeInstancesErrorSequence) > 0 {
|
||||||
|
err := m.DescribeInstancesErrorSequence[0]
|
||||||
|
m.DescribeInstancesErrorSequence = m.DescribeInstancesErrorSequence[1:]
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if m.DescribeInstancesError != nil {
|
if m.DescribeInstancesError != nil {
|
||||||
return nil, m.DescribeInstancesError
|
return nil, m.DescribeInstancesError
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,304 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package testing
|
|
||||||
|
|
||||||
// This file contains table-driven test infrastructure for AssumeRole and
|
|
||||||
// cross-role (two-hop) Dynamic Host Catalog scenarios. Each plugin operation
|
|
||||||
// has a corresponding case type and runner function. The orchestrator in
|
|
||||||
// e2e_host_test.go populates the case tables using the Terraform outputs.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Attr builders
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// roleArnAttrs builds catalog attributes for a single-hop AssumeRole catalog.
|
|
||||||
func roleArnAttrs(region, roleArn string) map[string]any {
|
|
||||||
return map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: true,
|
|
||||||
credential.ConstRoleArn: roleArn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// crossRoleAttrs builds catalog attributes for a two-hop cross-role catalog.
|
|
||||||
func crossRoleAttrs(region, principalRoleArn, targetRoleArn string) map[string]any {
|
|
||||||
return map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: true,
|
|
||||||
credential.ConstRoleArn: principalRoleArn,
|
|
||||||
credential.ConstTargetRoleArn: targetRoleArn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// tagFilterSetAttrs builds set attributes with a single tag-key filter.
|
|
||||||
func tagFilterSetAttrs(tags []string) map[string]any {
|
|
||||||
return map[string]any{
|
|
||||||
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("tag-key=%s", strings.Join(tags, ","))},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// requireTagInstancesMatchProvisioned asserts that every instance ID in tagInstancesMap
|
|
||||||
// is present in provisionedIds (the raw []any slice from a Terraform output).
|
|
||||||
func requireTagInstancesMatchProvisioned(t *testing.T, provisionedIds []any, tagInstancesMap map[string][]string) {
|
|
||||||
t.Helper()
|
|
||||||
idSet := make(map[string]struct{}, len(provisionedIds))
|
|
||||||
for _, id := range provisionedIds {
|
|
||||||
idSet[id.(string)] = struct{}{}
|
|
||||||
}
|
|
||||||
for tag, ids := range tagInstancesMap {
|
|
||||||
for _, id := range ids {
|
|
||||||
require.Contains(t, idSet, id, "instance ID %s in tag bucket %q was not provisioned by Terraform", id, tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildExpectedTagInstancesMap builds a map of tag key to instance IDs from a Terraform instance_tags output map.
|
|
||||||
func buildExpectedTagInstancesMap(rawTags map[string]any, expectedTags []string) map[string][]string {
|
|
||||||
tagMap := make(map[string][]string)
|
|
||||||
for instanceId, instanceTags := range rawTags {
|
|
||||||
for tagKey := range instanceTags.(map[string]any) {
|
|
||||||
for _, expectedTag := range expectedTags {
|
|
||||||
if tagKey == expectedTag {
|
|
||||||
tagMap[tagKey] = append(tagMap[tagKey], instanceId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tagMap
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Table-driven runners
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type onCreateCatalogCase struct {
|
|
||||||
name string
|
|
||||||
catalogAttrs map[string]any
|
|
||||||
wantErr string // empty = expect success with empty persisted secrets
|
|
||||||
}
|
|
||||||
|
|
||||||
func testOnCreateCatalogCases(ctx context.Context, t *testing.T, p *host.HostPlugin, cases []onCreateCatalogCase) {
|
|
||||||
t.Helper()
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
attrs, err := structpb.NewStruct(tc.catalogAttrs)
|
|
||||||
require.NoError(err)
|
|
||||||
resp, err := p.OnCreateCatalog(ctx, &pb.OnCreateCatalogRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{Attributes: attrs},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if tc.wantErr != "" {
|
|
||||||
require.Error(err)
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
require.True(ok)
|
|
||||||
require.Contains(st.Message(), tc.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(resp)
|
|
||||||
// Dynamic credentials are never stored — persisted secrets should be empty.
|
|
||||||
require.Empty(resp.GetPersisted().GetSecrets().GetFields())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type onUpdateCatalogCase struct {
|
|
||||||
name string
|
|
||||||
catalogAttrs map[string]any // used for both current and new when currentAttrs/newAttrs are nil
|
|
||||||
currentAttrs map[string]any // optional: override current catalog attrs
|
|
||||||
newAttrs map[string]any // optional: override new catalog attrs
|
|
||||||
wantErr string
|
|
||||||
}
|
|
||||||
|
|
||||||
func testOnUpdateCatalogCases(ctx context.Context, t *testing.T, p *host.HostPlugin, cases []onUpdateCatalogCase) {
|
|
||||||
t.Helper()
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
currentRaw := tc.catalogAttrs
|
|
||||||
if tc.currentAttrs != nil {
|
|
||||||
currentRaw = tc.currentAttrs
|
|
||||||
}
|
|
||||||
newRaw := tc.catalogAttrs
|
|
||||||
if tc.newAttrs != nil {
|
|
||||||
newRaw = tc.newAttrs
|
|
||||||
}
|
|
||||||
currentAttrs, err := structpb.NewStruct(currentRaw)
|
|
||||||
require.NoError(err)
|
|
||||||
newAttrs, err := structpb.NewStruct(newRaw)
|
|
||||||
require.NoError(err)
|
|
||||||
resp, err := p.OnUpdateCatalog(ctx, &pb.OnUpdateCatalogRequest{
|
|
||||||
CurrentCatalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{Attributes: currentAttrs},
|
|
||||||
},
|
|
||||||
NewCatalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{Attributes: newAttrs},
|
|
||||||
},
|
|
||||||
Persisted: &pb.HostCatalogPersisted{},
|
|
||||||
})
|
|
||||||
if tc.wantErr != "" {
|
|
||||||
require.Error(err)
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
require.True(ok)
|
|
||||||
require.Contains(st.Message(), tc.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(resp)
|
|
||||||
// Dynamic credentials are never stored — persisted secrets should be empty.
|
|
||||||
require.Empty(resp.GetPersisted().GetSecrets().GetFields())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type onSetCase struct {
|
|
||||||
name string
|
|
||||||
catalogAttrs map[string]any
|
|
||||||
tags []string // nil = no filter (empty set attrs)
|
|
||||||
wantErr string
|
|
||||||
}
|
|
||||||
|
|
||||||
func testOnCreateSetCases(ctx context.Context, t *testing.T, p *host.HostPlugin, cases []onSetCase) {
|
|
||||||
t.Helper()
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
catalogAttrs, err := structpb.NewStruct(tc.catalogAttrs)
|
|
||||||
require.NoError(err)
|
|
||||||
setRaw := map[string]any{host.ConstDescribeInstancesFilters: []any{}}
|
|
||||||
if len(tc.tags) > 0 {
|
|
||||||
setRaw = tagFilterSetAttrs(tc.tags)
|
|
||||||
}
|
|
||||||
setAttrs, err := structpb.NewStruct(setRaw)
|
|
||||||
require.NoError(err)
|
|
||||||
resp, err := p.OnCreateSet(ctx, &pb.OnCreateSetRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{Attrs: &hostcatalogs.HostCatalog_Attributes{Attributes: catalogAttrs}},
|
|
||||||
Set: &hostsets.HostSet{Attrs: &hostsets.HostSet_Attributes{Attributes: setAttrs}},
|
|
||||||
Persisted: &pb.HostCatalogPersisted{},
|
|
||||||
})
|
|
||||||
if tc.wantErr != "" {
|
|
||||||
require.Error(err)
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
require.True(ok)
|
|
||||||
require.Contains(st.Message(), tc.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testOnUpdateSetCases(ctx context.Context, t *testing.T, p *host.HostPlugin, cases []onSetCase) {
|
|
||||||
t.Helper()
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
catalogAttrs, err := structpb.NewStruct(tc.catalogAttrs)
|
|
||||||
require.NoError(err)
|
|
||||||
setRaw := map[string]any{host.ConstDescribeInstancesFilters: []any{}}
|
|
||||||
if len(tc.tags) > 0 {
|
|
||||||
setRaw = tagFilterSetAttrs(tc.tags)
|
|
||||||
}
|
|
||||||
setAttrs, err := structpb.NewStruct(setRaw)
|
|
||||||
require.NoError(err)
|
|
||||||
set := &hostsets.HostSet{Attrs: &hostsets.HostSet_Attributes{Attributes: setAttrs}}
|
|
||||||
resp, err := p.OnUpdateSet(ctx, &pb.OnUpdateSetRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{Attrs: &hostcatalogs.HostCatalog_Attributes{Attributes: catalogAttrs}},
|
|
||||||
CurrentSet: set,
|
|
||||||
NewSet: set,
|
|
||||||
Persisted: &pb.HostCatalogPersisted{},
|
|
||||||
})
|
|
||||||
if tc.wantErr != "" {
|
|
||||||
require.Error(err)
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
require.True(ok)
|
|
||||||
require.Contains(st.Message(), tc.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(resp)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type listHostsCase struct {
|
|
||||||
name string
|
|
||||||
catalogAttrs map[string]any
|
|
||||||
tags []string
|
|
||||||
expected map[string][]string // nil = don't assert instances (error cases)
|
|
||||||
wantErr string
|
|
||||||
}
|
|
||||||
|
|
||||||
func testListHostsCases(ctx context.Context, t *testing.T, p *host.HostPlugin, cases []listHostsCase) {
|
|
||||||
t.Helper()
|
|
||||||
for _, tc := range cases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Helper()
|
|
||||||
require := require.New(t)
|
|
||||||
catalogAttrs, err := structpb.NewStruct(tc.catalogAttrs)
|
|
||||||
require.NoError(err)
|
|
||||||
sets := make([]*hostsets.HostSet, len(tc.tags))
|
|
||||||
for i, tag := range tc.tags {
|
|
||||||
setAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("tag-key=%s", tag)},
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
sets[i] = &hostsets.HostSet{
|
|
||||||
Id: fmt.Sprintf("hostset-%d", i),
|
|
||||||
Attrs: &hostsets.HostSet_Attributes{Attributes: setAttrs},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp, err := p.ListHosts(ctx, &pb.ListHostsRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{Attrs: &hostcatalogs.HostCatalog_Attributes{Attributes: catalogAttrs}},
|
|
||||||
Sets: sets,
|
|
||||||
Persisted: &pb.HostCatalogPersisted{},
|
|
||||||
})
|
|
||||||
if tc.wantErr != "" {
|
|
||||||
require.Error(err)
|
|
||||||
st, ok := status.FromError(err)
|
|
||||||
require.True(ok)
|
|
||||||
require.Contains(st.Message(), tc.wantErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(resp)
|
|
||||||
expectedInstances := make(map[string][]string)
|
|
||||||
for i, tag := range tc.tags {
|
|
||||||
for _, instanceId := range tc.expected[tag] {
|
|
||||||
expectedInstances[instanceId] = append(expectedInstances[instanceId], fmt.Sprintf("hostset-%d", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
actualInstances := make(map[string][]string)
|
|
||||||
for _, h := range resp.GetHosts() {
|
|
||||||
actualInstances[h.ExternalId] = h.SetIds
|
|
||||||
}
|
|
||||||
require.Equal(expectedInstances, actualInstances)
|
|
||||||
t.Logf("success: %d hosts matched", len(actualInstances))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
// Copyright IBM Corp. 2021, 2026
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package testing
|
|
||||||
|
|
||||||
// This file contains helper functions for the static-credential (access key /
|
|
||||||
// secret access key) test paths exercised by TestHostPlugin in e2e_host_test.go.
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
||||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
|
||||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/protobuf/types/known/structpb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing OnCreateCatalog (region=%s, rotate=%t)", region, rotate)
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotate,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
request := &pb.OnCreateCatalogRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
},
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := p.OnCreateCatalog(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(response)
|
|
||||||
persisted := response.GetPersisted()
|
|
||||||
require.NotNil(persisted)
|
|
||||||
return validatePersistedSecrets(t, persisted.GetSecrets(), accessKeyId, secretAccessKey, rotate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPluginOnUpdateCatalog(
|
|
||||||
ctx context.Context, t *testing.T, p *host.HostPlugin,
|
|
||||||
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
|
||||||
rotated, rotate bool,
|
|
||||||
) (string, string) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing OnUpdateCatalog (region=%s, newcreds=%t, rotated=%t, rotate=%t)", region, newAccessKeyId != "" && newSecretAccessKey != "", rotated, rotate)
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
// Take a timestamp of the current time to get a point in time to
|
|
||||||
// reference, ensuring that we are updating credential rotation
|
|
||||||
// timestamps.
|
|
||||||
currentCredsLastRotatedTime := time.Now()
|
|
||||||
|
|
||||||
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotated,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqNewAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotate,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
var reqSecrets *structpb.Struct
|
|
||||||
if newAccessKeyId != "" && newSecretAccessKey != "" {
|
|
||||||
reqSecrets, err = structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: newAccessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: newSecretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
}
|
|
||||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: currentAccessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: currentSecretAccessKey,
|
|
||||||
credential.ConstCredsLastRotatedTime: func() string {
|
|
||||||
if rotated {
|
|
||||||
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (time.Time{}).Format(time.RFC3339Nano)
|
|
||||||
}(),
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(reqPersistedSecrets)
|
|
||||||
request := &pb.OnUpdateCatalogRequest{
|
|
||||||
CurrentCatalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: reqCurrentAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NewCatalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: reqNewAttrs,
|
|
||||||
},
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
Persisted: &pb.HostCatalogPersisted{
|
|
||||||
Secrets: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := p.OnUpdateCatalog(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(response)
|
|
||||||
persisted := response.GetPersisted()
|
|
||||||
require.NotNil(persisted)
|
|
||||||
return validateUpdateSecrets(t, persisted.GetSecrets(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing OnDeleteCatalog (region=%s, rotated=%t)", region, rotated)
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
reqAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: !rotated,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
credential.ConstCredsLastRotatedTime: func() string {
|
|
||||||
if rotated {
|
|
||||||
return time.Now().Format(time.RFC3339Nano)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (time.Time{}).Format(time.RFC3339Nano)
|
|
||||||
}(),
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
request := &pb.OnDeleteCatalogRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: reqAttrs,
|
|
||||||
},
|
|
||||||
Secrets: reqSecrets,
|
|
||||||
},
|
|
||||||
Persisted: &pb.HostCatalogPersisted{
|
|
||||||
Secrets: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := p.OnDeleteCatalog(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(response)
|
|
||||||
|
|
||||||
// We want to test the validity of the credentials post-deletion.
|
|
||||||
if rotated {
|
|
||||||
// The credentials should no longer be valid.
|
|
||||||
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
|
||||||
} else {
|
|
||||||
// The credentials should still be valid. Sleep 10s first just to
|
|
||||||
// be sure, since we're not rotating.
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
requireCredentialsValid(t, accessKeyId, secretAccessKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPluginOnCreateUpdateSet(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, tags []string) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing OnCreateSet (region=%s, tags=%v)", region, tags)
|
|
||||||
require := require.New(t)
|
|
||||||
catalogAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: true, // Note that this does nothing in sets, but just noting for tests
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
setAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("tag-key=%s", strings.Join(tags, ","))},
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
createRequest := &pb.OnCreateSetRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: catalogAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Set: &hostsets.HostSet{
|
|
||||||
Attrs: &hostsets.HostSet_Attributes{
|
|
||||||
Attributes: setAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Persisted: &pb.HostCatalogPersisted{
|
|
||||||
Secrets: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
createResponse, err := p.OnCreateSet(ctx, createRequest)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(createResponse)
|
|
||||||
|
|
||||||
// Do an update test in the same function, as it's pretty much the
|
|
||||||
// same function right now.
|
|
||||||
t.Logf("testing OnUpdateSet (region=%s, tags=%v)", region, tags)
|
|
||||||
updateRequest := &pb.OnUpdateSetRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: catalogAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
CurrentSet: &hostsets.HostSet{
|
|
||||||
Attrs: &hostsets.HostSet_Attributes{
|
|
||||||
Attributes: setAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NewSet: &hostsets.HostSet{
|
|
||||||
Attrs: &hostsets.HostSet_Attributes{
|
|
||||||
Attributes: setAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Persisted: &pb.HostCatalogPersisted{
|
|
||||||
Secrets: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
updateResponse, err := p.OnUpdateSet(ctx, updateRequest)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(updateResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testPluginListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, tags []string, expected map[string][]string) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing ListHosts (region=%s, tags=%v)", region, tags)
|
|
||||||
require := require.New(t)
|
|
||||||
catalogAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: true, // Note that this does nothing in sets, but just noting for tests
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
sets := make([]*hostsets.HostSet, len(tags))
|
|
||||||
for i, tag := range tags {
|
|
||||||
setAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("tag-key=%s", tag)},
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
sets[i] = &hostsets.HostSet{
|
|
||||||
Id: fmt.Sprintf("hostset-%d", i),
|
|
||||||
Attrs: &hostsets.HostSet_Attributes{
|
|
||||||
Attributes: setAttrs,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
request := &pb.ListHostsRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: catalogAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Sets: sets,
|
|
||||||
Persisted: &pb.HostCatalogPersisted{
|
|
||||||
Secrets: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
response, err := p.ListHosts(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.NotNil(response)
|
|
||||||
|
|
||||||
// Validate the returned instances by ID. Assemble the instance
|
|
||||||
// details from the expected set.
|
|
||||||
expectedInstances := make(map[string][]string)
|
|
||||||
for i, tag := range tags {
|
|
||||||
for _, instanceId := range expected[tag] {
|
|
||||||
expectedInstances[instanceId] = append(expectedInstances[instanceId], fmt.Sprintf("hostset-%d", i))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Take the returned hosts by ID and create the same kind of map.
|
|
||||||
actualInstances := make(map[string][]string)
|
|
||||||
for _, host := range response.GetHosts() {
|
|
||||||
actualInstances[host.ExternalId] = host.SetIds
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare
|
|
||||||
require.Equal(expectedInstances, actualInstances)
|
|
||||||
// Success
|
|
||||||
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testInstanceAddressesOnlyListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin, tf *TestTerraformer, region, accessKeyId, secretAccessKey string) {
|
|
||||||
t.Helper()
|
|
||||||
t.Logf("testing testInstanceAddressesOnlyListHosts")
|
|
||||||
|
|
||||||
// Get the instance details from tf output
|
|
||||||
instanceID, err := tf.GetOutputString("multi_address_instance_id")
|
|
||||||
require.NoError(t, err)
|
|
||||||
primaryAddresses, err := tf.GetOutputMap("multi_address_primary_addresses")
|
|
||||||
require.NoError(t, err)
|
|
||||||
secondaryAddresses, err := tf.GetOutputMap("multi_address_secondary_addresses")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var primary, allAddresses []string
|
|
||||||
for _, addr := range primaryAddresses {
|
|
||||||
primary = append(primary, addr.(string))
|
|
||||||
}
|
|
||||||
allAddresses = append(allAddresses, primary...)
|
|
||||||
for _, addr := range secondaryAddresses {
|
|
||||||
allAddresses = append(allAddresses, addr.(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
name string
|
|
||||||
instanceAddressesOnly bool
|
|
||||||
expectedIPs []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "all addresses",
|
|
||||||
expectedIPs: allAddresses,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "instance addresses only",
|
|
||||||
instanceAddressesOnly: true,
|
|
||||||
expectedIPs: primary,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range cases {
|
|
||||||
tc := tc
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
catalogAttrMap := map[string]any{
|
|
||||||
credential.ConstRegion: region,
|
|
||||||
credential.ConstDisableCredentialRotation: true,
|
|
||||||
}
|
|
||||||
if tc.instanceAddressesOnly {
|
|
||||||
catalogAttrMap[host.ConstInstanceAddressesOnly] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
catalogAttrs, err := structpb.NewStruct(catalogAttrMap)
|
|
||||||
require.NoError(err)
|
|
||||||
setAttrs, err := structpb.NewStruct(map[string]any{
|
|
||||||
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("instance-id=%s", instanceID)},
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
|
||||||
credential.ConstAccessKeyId: accessKeyId,
|
|
||||||
credential.ConstSecretAccessKey: secretAccessKey,
|
|
||||||
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
|
||||||
})
|
|
||||||
require.NoError(err)
|
|
||||||
request := &pb.ListHostsRequest{
|
|
||||||
Catalog: &hostcatalogs.HostCatalog{
|
|
||||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
|
||||||
Attributes: catalogAttrs,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Sets: []*hostsets.HostSet{{
|
|
||||||
Id: "hostset-0",
|
|
||||||
Attrs: &hostsets.HostSet_Attributes{Attributes: setAttrs},
|
|
||||||
}},
|
|
||||||
Persisted: &pb.HostCatalogPersisted{
|
|
||||||
Secrets: reqPersistedSecrets,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
response, err := p.ListHosts(ctx, request)
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(response.GetHosts(), 1)
|
|
||||||
hostResp := response.GetHosts()[0]
|
|
||||||
|
|
||||||
require.Equal(instanceID, hostResp.ExternalId)
|
|
||||||
require.ElementsMatch(hostResp.IpAddresses, tc.expectedIPs)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+370
-173
@@ -3,24 +3,21 @@
|
|||||||
|
|
||||||
package testing
|
package testing
|
||||||
|
|
||||||
// This file contains the TestHostPlugin orchestrator. It deploys the Terraform
|
|
||||||
// workspace, collects outputs, and calls the per-credential-type helper
|
|
||||||
// functions defined in e2e_host_static_test.go and e2e_host_assumerole_test.go.
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
||||||
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
"github.com/hashicorp/boundary-plugin-aws/plugin/service/host"
|
||||||
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
|
||||||
|
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||||
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
|
|
||||||
const (
|
|
||||||
envTargetAccessKeyId = "TARGET_AWS_ACCESS_KEY_ID"
|
|
||||||
envTargetSecretAccessKey = "TARGET_AWS_SECRET_ACCESS_KEY"
|
|
||||||
envTargetSessionToken = "TARGET_AWS_SESSION_TOKEN"
|
|
||||||
envTargetRegion = "TARGET_AWS_REGION"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const expectedEc2InstanceCount = 5
|
const expectedEc2InstanceCount = 5
|
||||||
@@ -38,29 +35,7 @@ func TestHostPlugin(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
tf, err := NewTestTerraformer("testdata/host")
|
||||||
// Collect cross-account credentials up front so they can be passed as
|
|
||||||
// Terraform variables. The cross-account section is skipped (not failed)
|
|
||||||
// when these are absent, but we need the values available before Deploy.
|
|
||||||
targetAccessKeyId := os.Getenv(envTargetAccessKeyId)
|
|
||||||
targetSecretAccessKey := os.Getenv(envTargetSecretAccessKey)
|
|
||||||
targetSessionToken := os.Getenv(envTargetSessionToken)
|
|
||||||
targetRegion := os.Getenv(envTargetRegion)
|
|
||||||
if targetRegion == "" {
|
|
||||||
targetRegion = region
|
|
||||||
}
|
|
||||||
|
|
||||||
tfVars := map[string]string{}
|
|
||||||
if targetAccessKeyId != "" && targetSecretAccessKey != "" {
|
|
||||||
tfVars["target_access_key_id"] = targetAccessKeyId
|
|
||||||
tfVars["target_secret_access_key"] = targetSecretAccessKey
|
|
||||||
tfVars["target_region"] = targetRegion
|
|
||||||
if targetSessionToken != "" {
|
|
||||||
tfVars["target_session_token"] = targetSessionToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tf, err := NewTestTerraformer("testdata/host", tfVars)
|
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
require.NotNil(tf)
|
require.NotNil(tf)
|
||||||
|
|
||||||
@@ -162,9 +137,16 @@ func TestHostPlugin(t *testing.T) {
|
|||||||
// Reassign the keyid and secret first.
|
// Reassign the keyid and secret first.
|
||||||
keyid, secret = iamAccessKeyIds[5].(string), iamSecretAccessKeys[5].(string)
|
keyid, secret = iamAccessKeyIds[5].(string), iamSecretAccessKeys[5].(string)
|
||||||
// Process the collection of instances and index by expected tag names.
|
// Process the collection of instances and index by expected tag names.
|
||||||
expectedTagInstancesMap := buildExpectedTagInstancesMap(ec2InstanceTags, expectedTags)
|
expectedTagInstancesMap := make(map[string][]string)
|
||||||
|
for instanceId, instanceTags := range ec2InstanceTags {
|
||||||
requireTagInstancesMatchProvisioned(t, ec2InstanceIds, expectedTagInstancesMap)
|
for tagKey := range instanceTags.(map[string]any) {
|
||||||
|
for _, expectedTag := range expectedTags {
|
||||||
|
if tagKey == expectedTag {
|
||||||
|
expectedTagInstancesMap[tagKey] = append(expectedTagInstancesMap[tagKey], instanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cases := [][]string{
|
cases := [][]string{
|
||||||
{expectedTags[0]},
|
{expectedTags[0]},
|
||||||
@@ -186,162 +168,377 @@ func TestHostPlugin(t *testing.T) {
|
|||||||
|
|
||||||
// finally lets test instance_addresses_only catalog attribute correctly syncs only instance addresses
|
// finally lets test instance_addresses_only catalog attribute correctly syncs only instance addresses
|
||||||
testInstanceAddressesOnlyListHosts(ctx, t, p, tf, region, keyid, secret)
|
testInstanceAddressesOnlyListHosts(ctx, t, p, tf, region, keyid, secret)
|
||||||
|
}
|
||||||
|
|
||||||
// ********************
|
func testPluginOnCreateCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
||||||
// * AssumeRole - Happy Path & Error Cases
|
t.Helper()
|
||||||
// ********************
|
t.Logf("testing OnCreateCatalog (region=%s, rotate=%t)", region, rotate)
|
||||||
assumeRoleArn, err := tf.GetOutputString("assume_role_arn")
|
require := require.New(t)
|
||||||
|
|
||||||
|
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstRegion: region,
|
||||||
|
credential.ConstDisableCredentialRotation: !rotate,
|
||||||
|
})
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
noTrustRoleArn, err := tf.GetOutputString("assume_role_no_trust_arn")
|
credential.ConstAccessKeyId: accessKeyId,
|
||||||
|
credential.ConstSecretAccessKey: secretAccessKey,
|
||||||
|
})
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
request := &pb.OnCreateCatalogRequest{
|
||||||
missingPermRoleArn, err := tf.GetOutputString("assume_role_no_ec2_permission_arn")
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: reqAttrs,
|
||||||
|
},
|
||||||
|
Secrets: reqSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := p.OnCreateCatalog(ctx, request)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
require.NotNil(response)
|
||||||
|
persisted := response.GetPersisted()
|
||||||
|
require.NotNil(persisted)
|
||||||
|
return validatePersistedSecrets(t, persisted.GetSecrets(), accessKeyId, secretAccessKey, rotate)
|
||||||
|
}
|
||||||
|
|
||||||
testOnCreateCatalogCases(ctx, t, p, []onCreateCatalogCase{
|
func testPluginOnUpdateCatalog(
|
||||||
{name: "assume role happy path", catalogAttrs: roleArnAttrs(region, assumeRoleArn)},
|
ctx context.Context, t *testing.T, p *host.HostPlugin,
|
||||||
{name: "assume role no trust", catalogAttrs: roleArnAttrs(region, noTrustRoleArn), wantErr: "AccessDenied"},
|
region, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
||||||
{name: "assume role missing ec2 permission", catalogAttrs: roleArnAttrs(region, missingPermRoleArn), wantErr: "UnauthorizedOperation"},
|
rotated, rotate bool,
|
||||||
})
|
) (string, string) {
|
||||||
testOnUpdateCatalogCases(ctx, t, p, []onUpdateCatalogCase{
|
t.Helper()
|
||||||
{name: "assume role happy path", catalogAttrs: roleArnAttrs(region, assumeRoleArn)},
|
t.Logf("testing OnUpdateCatalog (region=%s, newcreds=%t, rotated=%t, rotate=%t)", region, newAccessKeyId != "" && newSecretAccessKey != "", rotated, rotate)
|
||||||
{name: "assume role no trust", catalogAttrs: roleArnAttrs(region, noTrustRoleArn), wantErr: "AccessDenied"},
|
require := require.New(t)
|
||||||
{name: "assume role missing ec2 permission", currentAttrs: roleArnAttrs(region, assumeRoleArn), newAttrs: roleArnAttrs(region, missingPermRoleArn), wantErr: "UnauthorizedOperation"},
|
|
||||||
})
|
|
||||||
testOnCreateSetCases(ctx, t, p, []onSetCase{
|
|
||||||
{name: "assume role happy path", catalogAttrs: roleArnAttrs(region, assumeRoleArn), tags: cases[0]},
|
|
||||||
{name: "assume role no trust", catalogAttrs: roleArnAttrs(region, noTrustRoleArn), wantErr: "AccessDenied"},
|
|
||||||
{name: "assume role missing ec2 permission", catalogAttrs: roleArnAttrs(region, missingPermRoleArn), wantErr: "UnauthorizedOperation"},
|
|
||||||
})
|
|
||||||
testOnUpdateSetCases(ctx, t, p, []onSetCase{
|
|
||||||
{name: "assume role happy path", catalogAttrs: roleArnAttrs(region, assumeRoleArn), tags: cases[0]},
|
|
||||||
{name: "assume role no trust", catalogAttrs: roleArnAttrs(region, noTrustRoleArn), wantErr: "AccessDenied"},
|
|
||||||
{name: "assume role missing ec2 permission", catalogAttrs: roleArnAttrs(region, missingPermRoleArn), wantErr: "UnauthorizedOperation"},
|
|
||||||
})
|
|
||||||
testListHostsCases(ctx, t, p, []listHostsCase{
|
|
||||||
{name: "assume role happy path", catalogAttrs: roleArnAttrs(region, assumeRoleArn), tags: cases[0], expected: expectedTagInstancesMap},
|
|
||||||
{name: "assume role no trust", catalogAttrs: roleArnAttrs(region, noTrustRoleArn), tags: cases[0], wantErr: "AccessDenied"},
|
|
||||||
{name: "assume role missing ec2 permission", catalogAttrs: roleArnAttrs(region, missingPermRoleArn), tags: cases[0], wantErr: "UnauthorizedOperation"},
|
|
||||||
})
|
|
||||||
|
|
||||||
// ********************
|
// Take a timestamp of the current time to get a point in time to
|
||||||
// * Cross-Role AssumeRole - Happy Path & Error Cases
|
// reference, ensuring that we are updating credential rotation
|
||||||
// ********************
|
// timestamps.
|
||||||
// These tests exercise the two-hop role chain within a single AWS account.
|
currentCredsLastRotatedTime := time.Now()
|
||||||
// The principal role has no ec2:DescribeInstances; the target role
|
|
||||||
// (assume_role) does. The plugin must assume the principal first, then use
|
reqCurrentAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
// those credentials to assume the target role before calling DescribeInstances.
|
credential.ConstRegion: region,
|
||||||
// This validates the plugin's chained credential logic without requiring a
|
credential.ConstDisableCredentialRotation: !rotated,
|
||||||
// second AWS account.
|
})
|
||||||
crossRolePrincipalArn, err := tf.GetOutputString("cross_role_principal_arn")
|
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
reqNewAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
crossRolePrincipalNoTrustArn, err := tf.GetOutputString("cross_role_principal_no_trust_arn")
|
credential.ConstRegion: region,
|
||||||
|
credential.ConstDisableCredentialRotation: !rotate,
|
||||||
|
})
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
var reqSecrets *structpb.Struct
|
||||||
|
if newAccessKeyId != "" && newSecretAccessKey != "" {
|
||||||
|
reqSecrets, err = structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstAccessKeyId: newAccessKeyId,
|
||||||
|
credential.ConstSecretAccessKey: newSecretAccessKey,
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
}
|
||||||
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstAccessKeyId: currentAccessKeyId,
|
||||||
|
credential.ConstSecretAccessKey: currentSecretAccessKey,
|
||||||
|
credential.ConstCredsLastRotatedTime: func() string {
|
||||||
|
if rotated {
|
||||||
|
return currentCredsLastRotatedTime.Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
crossRoleTargetNoTrustArn, err := tf.GetOutputString("cross_role_target_no_trust_arn")
|
return (time.Time{}).Format(time.RFC3339Nano)
|
||||||
|
}(),
|
||||||
|
})
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
require.NotNil(reqPersistedSecrets)
|
||||||
crossRoleTargetNoEc2PermissionArn, err := tf.GetOutputString("cross_role_target_no_ec2_permission_arn")
|
request := &pb.OnUpdateCatalogRequest{
|
||||||
|
CurrentCatalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: reqCurrentAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewCatalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: reqNewAttrs,
|
||||||
|
},
|
||||||
|
Secrets: reqSecrets,
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := p.OnUpdateCatalog(ctx, request)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
|
require.NotNil(response)
|
||||||
|
persisted := response.GetPersisted()
|
||||||
|
require.NotNil(persisted)
|
||||||
|
return validateUpdateSecrets(t, persisted.GetSecrets(), currentCredsLastRotatedTime, currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey, rotated, rotate)
|
||||||
|
}
|
||||||
|
|
||||||
// assume_role_arn is reused as the cross-role target — it has ec2:DescribeInstances
|
func testPluginOnDeleteCatalog(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, rotated bool) {
|
||||||
// and its trust policy allows the cross-role principal to assume it.
|
t.Helper()
|
||||||
testOnCreateCatalogCases(ctx, t, p, []onCreateCatalogCase{
|
t.Logf("testing OnDeleteCatalog (region=%s, rotated=%t)", region, rotated)
|
||||||
{name: "cross-role happy path", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, assumeRoleArn)},
|
require := require.New(t)
|
||||||
{name: "cross-role principal no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalNoTrustArn, assumeRoleArn), wantErr: "AccessDenied"},
|
|
||||||
{name: "cross-role target no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoTrustArn), wantErr: "AccessDenied"},
|
reqAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
{name: "cross-role target missing ec2 permission", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
credential.ConstRegion: region,
|
||||||
|
credential.ConstDisableCredentialRotation: !rotated,
|
||||||
})
|
})
|
||||||
testOnUpdateCatalogCases(ctx, t, p, []onUpdateCatalogCase{
|
require.NoError(err)
|
||||||
{name: "cross-role happy path", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, assumeRoleArn)},
|
reqSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
{name: "cross-role principal no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalNoTrustArn, assumeRoleArn), wantErr: "AccessDenied"},
|
credential.ConstAccessKeyId: accessKeyId,
|
||||||
{name: "cross-role target no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoTrustArn), wantErr: "AccessDenied"},
|
credential.ConstSecretAccessKey: secretAccessKey,
|
||||||
{name: "cross-role target missing ec2 permission", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
|
||||||
})
|
})
|
||||||
testOnCreateSetCases(ctx, t, p, []onSetCase{
|
require.NoError(err)
|
||||||
{name: "cross-role happy path", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, assumeRoleArn), tags: cases[0]},
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
{name: "cross-role principal no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalNoTrustArn, assumeRoleArn), wantErr: "AccessDenied"},
|
credential.ConstAccessKeyId: accessKeyId,
|
||||||
{name: "cross-role target no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoTrustArn), wantErr: "AccessDenied"},
|
credential.ConstSecretAccessKey: secretAccessKey,
|
||||||
{name: "cross-role target missing ec2 permission", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
credential.ConstCredsLastRotatedTime: func() string {
|
||||||
|
if rotated {
|
||||||
|
return time.Now().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (time.Time{}).Format(time.RFC3339Nano)
|
||||||
|
}(),
|
||||||
})
|
})
|
||||||
testOnUpdateSetCases(ctx, t, p, []onSetCase{
|
require.NoError(err)
|
||||||
{name: "cross-role happy path", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, assumeRoleArn), tags: cases[0]},
|
request := &pb.OnDeleteCatalogRequest{
|
||||||
{name: "cross-role principal no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalNoTrustArn, assumeRoleArn), wantErr: "AccessDenied"},
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
{name: "cross-role target no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoTrustArn), wantErr: "AccessDenied"},
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
{name: "cross-role target missing ec2 permission", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
Attributes: reqAttrs,
|
||||||
})
|
},
|
||||||
testListHostsCases(ctx, t, p, []listHostsCase{
|
Secrets: reqSecrets,
|
||||||
{name: "cross-role happy path", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, assumeRoleArn), tags: cases[0], expected: expectedTagInstancesMap},
|
},
|
||||||
{name: "cross-role principal no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalNoTrustArn, assumeRoleArn), tags: cases[0], wantErr: "AccessDenied"},
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
{name: "cross-role target no trust", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoTrustArn), tags: cases[0], wantErr: "AccessDenied"},
|
Secrets: reqPersistedSecrets,
|
||||||
{name: "cross-role target missing ec2 permission", catalogAttrs: crossRoleAttrs(region, crossRolePrincipalArn, crossRoleTargetNoEc2PermissionArn), tags: cases[0], wantErr: "UnauthorizedOperation"},
|
},
|
||||||
})
|
}
|
||||||
// ********************
|
response, err := p.OnDeleteCatalog(ctx, request)
|
||||||
// * Cross-Account AssumeRole - Happy Path & Error Cases
|
require.NoError(err)
|
||||||
// ********************
|
require.NotNil(response)
|
||||||
// These tests require credentials for a second AWS account supplied via
|
|
||||||
// TARGET_AWS_ACCESS_KEY_ID, TARGET_AWS_SECRET_ACCESS_KEY, and (for
|
// We want to test the validity of the credentials post-deletion.
|
||||||
// temporary credentials) TARGET_AWS_SESSION_TOKEN. They are skipped
|
if rotated {
|
||||||
// (not failed) when the access key ID or secret access key are absent.
|
// The credentials should no longer be valid.
|
||||||
if targetAccessKeyId == "" || targetSecretAccessKey == "" {
|
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
||||||
t.Log("skipping cross-account tests: set TARGET_AWS_ACCESS_KEY_ID and TARGET_AWS_SECRET_ACCESS_KEY to enable")
|
|
||||||
} else {
|
} else {
|
||||||
crossAccountPrincipalArn, err := tf.GetOutputString("cross_account_principal_arn")
|
// The credentials should still be valid. Sleep 10s first just to
|
||||||
require.NoError(err)
|
// be sure, since we're not rotating.
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
|
requireCredentialsValid(t, accessKeyId, secretAccessKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
crossAccountPrincipalNoTrustArn, err := tf.GetOutputString("cross_account_principal_no_trust_arn")
|
func testPluginOnCreateUpdateSet(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, tags []string) {
|
||||||
require.NoError(err)
|
t.Helper()
|
||||||
|
t.Logf("testing OnCreateSet (region=%s, tags=%v)", region, tags)
|
||||||
|
require := require.New(t)
|
||||||
|
catalogAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstRegion: region,
|
||||||
|
credential.ConstDisableCredentialRotation: true, // Note that this does nothing in sets, but just noting for tests
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
setAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("tag-key=%s", strings.Join(tags, ","))},
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstAccessKeyId: accessKeyId,
|
||||||
|
credential.ConstSecretAccessKey: secretAccessKey,
|
||||||
|
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
createRequest := &pb.OnCreateSetRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: catalogAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Set: &hostsets.HostSet{
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: setAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
createResponse, err := p.OnCreateSet(ctx, createRequest)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(createResponse)
|
||||||
|
|
||||||
crossAccountTargetArn, err := tf.GetOutputString("cross_account_target_arn")
|
// Do an update test in the same function, as it's pretty much the
|
||||||
require.NoError(err)
|
// same function right now.
|
||||||
|
t.Logf("testing OnUpdateSet (region=%s, tags=%v)", region, tags)
|
||||||
|
updateRequest := &pb.OnUpdateSetRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: catalogAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CurrentSet: &hostsets.HostSet{
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: setAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
NewSet: &hostsets.HostSet{
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
|
Attributes: setAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
updateResponse, err := p.OnUpdateSet(ctx, updateRequest)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(updateResponse)
|
||||||
|
}
|
||||||
|
|
||||||
crossAccountTargetNoTrustArn, err := tf.GetOutputString("cross_account_target_no_trust_arn")
|
func testPluginListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin, region, accessKeyId, secretAccessKey string, tags []string, expected map[string][]string) {
|
||||||
require.NoError(err)
|
t.Helper()
|
||||||
|
t.Logf("testing ListHosts (region=%s, tags=%v)", region, tags)
|
||||||
crossAccountTargetNoEc2PermissionArn, err := tf.GetOutputString("cross_account_target_no_ec2_permission_arn")
|
require := require.New(t)
|
||||||
require.NoError(err)
|
catalogAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstRegion: region,
|
||||||
targetEc2InstanceIds, err := tf.GetOutputSlice("target_instance_ids")
|
credential.ConstDisableCredentialRotation: true, // Note that this does nothing in sets, but just noting for tests
|
||||||
require.NoError(err)
|
})
|
||||||
require.Len(targetEc2InstanceIds, expectedEc2InstanceCount)
|
require.NoError(err)
|
||||||
|
sets := make([]*hostsets.HostSet, len(tags))
|
||||||
targetEc2InstanceTags, err := tf.GetOutputMap("target_instance_tags")
|
for i, tag := range tags {
|
||||||
require.NoError(err)
|
setAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
expectedTargetTagInstancesMap := buildExpectedTagInstancesMap(targetEc2InstanceTags, expectedTags)
|
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("tag-key=%s", tag)},
|
||||||
|
|
||||||
requireTagInstancesMatchProvisioned(t, targetEc2InstanceIds, expectedTargetTagInstancesMap)
|
|
||||||
|
|
||||||
testOnCreateCatalogCases(ctx, t, p, []onCreateCatalogCase{
|
|
||||||
{name: "cross-account happy path", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetArn)},
|
|
||||||
{name: "cross-account principal no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalNoTrustArn, crossAccountTargetArn), wantErr: "AccessDenied"},
|
|
||||||
{name: "cross-account target no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoTrustArn), wantErr: "AccessDenied"},
|
|
||||||
{name: "cross-account target missing ec2 permission", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
|
||||||
})
|
})
|
||||||
testOnUpdateCatalogCases(ctx, t, p, []onUpdateCatalogCase{
|
require.NoError(err)
|
||||||
{name: "cross-account happy path", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetArn)},
|
sets[i] = &hostsets.HostSet{
|
||||||
{name: "cross-account principal no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalNoTrustArn, crossAccountTargetArn), wantErr: "AccessDenied"},
|
Id: fmt.Sprintf("hostset-%d", i),
|
||||||
{name: "cross-account target no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoTrustArn), wantErr: "AccessDenied"},
|
Attrs: &hostsets.HostSet_Attributes{
|
||||||
{name: "cross-account target missing ec2 permission", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
Attributes: setAttrs,
|
||||||
})
|
},
|
||||||
testOnCreateSetCases(ctx, t, p, []onSetCase{
|
}
|
||||||
{name: "cross-account happy path", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetArn), tags: cases[0]},
|
}
|
||||||
{name: "cross-account principal no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalNoTrustArn, crossAccountTargetArn), wantErr: "AccessDenied"},
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
{name: "cross-account target no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoTrustArn), wantErr: "AccessDenied"},
|
credential.ConstAccessKeyId: accessKeyId,
|
||||||
{name: "cross-account target missing ec2 permission", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
credential.ConstSecretAccessKey: secretAccessKey,
|
||||||
})
|
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||||
testOnUpdateSetCases(ctx, t, p, []onSetCase{
|
})
|
||||||
{name: "cross-account happy path", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetArn), tags: cases[0]},
|
require.NoError(err)
|
||||||
{name: "cross-account principal no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalNoTrustArn, crossAccountTargetArn), wantErr: "AccessDenied"},
|
request := &pb.ListHostsRequest{
|
||||||
{name: "cross-account target no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoTrustArn), wantErr: "AccessDenied"},
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
{name: "cross-account target missing ec2 permission", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoEc2PermissionArn), wantErr: "UnauthorizedOperation"},
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
})
|
Attributes: catalogAttrs,
|
||||||
testListHostsCases(ctx, t, p, []listHostsCase{
|
},
|
||||||
{name: "cross-account happy path", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetArn), tags: cases[0], expected: expectedTargetTagInstancesMap},
|
},
|
||||||
{name: "cross-account principal no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalNoTrustArn, crossAccountTargetArn), tags: cases[0], wantErr: "AccessDenied"},
|
Sets: sets,
|
||||||
{name: "cross-account target no trust", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoTrustArn), tags: cases[0], wantErr: "AccessDenied"},
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
{name: "cross-account target missing ec2 permission", catalogAttrs: crossRoleAttrs(targetRegion, crossAccountPrincipalArn, crossAccountTargetNoEc2PermissionArn), tags: cases[0], wantErr: "UnauthorizedOperation"},
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
response, err := p.ListHosts(ctx, request)
|
||||||
|
require.NoError(err)
|
||||||
|
require.NotNil(response)
|
||||||
|
|
||||||
|
// Validate the returned instances by ID. Assemble the instance
|
||||||
|
// details from the expected set.
|
||||||
|
expectedInstances := make(map[string][]string)
|
||||||
|
for i, tag := range tags {
|
||||||
|
for _, instanceId := range expected[tag] {
|
||||||
|
expectedInstances[instanceId] = append(expectedInstances[instanceId], fmt.Sprintf("hostset-%d", i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the returned hosts by ID and create the same kind of map.
|
||||||
|
actualInstances := make(map[string][]string)
|
||||||
|
for _, host := range response.GetHosts() {
|
||||||
|
actualInstances[host.ExternalId] = host.SetIds
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare
|
||||||
|
require.Equal(expectedInstances, actualInstances)
|
||||||
|
// Success
|
||||||
|
t.Logf("testing ListHosts: success (region=%s, tags=%v, expected/actual=(len=%d, ids=%s))", region, tags, len(actualInstances), actualInstances)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInstanceAddressesOnlyListHosts(ctx context.Context, t *testing.T, p *host.HostPlugin, tf *TestTerraformer, region, accessKeyId, secretAccessKey string) {
|
||||||
|
t.Helper()
|
||||||
|
t.Logf("testing testInstanceAddressesOnlyListHosts")
|
||||||
|
|
||||||
|
// Get the instance details from tf output
|
||||||
|
instanceID, err := tf.GetOutputString("multi_address_instance_id")
|
||||||
|
require.NoError(t, err)
|
||||||
|
primaryAddresses, err := tf.GetOutputMap("multi_address_primary_addresses")
|
||||||
|
require.NoError(t, err)
|
||||||
|
secondaryAddresses, err := tf.GetOutputMap("multi_address_secondary_addresses")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var primary, allAddresses []string
|
||||||
|
for _, addr := range primaryAddresses {
|
||||||
|
primary = append(primary, addr.(string))
|
||||||
|
}
|
||||||
|
allAddresses = append(allAddresses, primary...)
|
||||||
|
for _, addr := range secondaryAddresses {
|
||||||
|
allAddresses = append(allAddresses, addr.(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
instanceAddressesOnly bool
|
||||||
|
expectedIPs []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all addresses",
|
||||||
|
expectedIPs: allAddresses,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instance addresses only",
|
||||||
|
instanceAddressesOnly: true,
|
||||||
|
expectedIPs: primary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
catalogAttrMap := map[string]any{
|
||||||
|
credential.ConstRegion: region,
|
||||||
|
credential.ConstDisableCredentialRotation: true,
|
||||||
|
}
|
||||||
|
if tc.instanceAddressesOnly {
|
||||||
|
catalogAttrMap[host.ConstInstanceAddressesOnly] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogAttrs, err := structpb.NewStruct(catalogAttrMap)
|
||||||
|
require.NoError(err)
|
||||||
|
setAttrs, err := structpb.NewStruct(map[string]any{
|
||||||
|
host.ConstDescribeInstancesFilters: []any{fmt.Sprintf("instance-id=%s", instanceID)},
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
|
||||||
|
reqPersistedSecrets, err := structpb.NewStruct(map[string]any{
|
||||||
|
credential.ConstAccessKeyId: accessKeyId,
|
||||||
|
credential.ConstSecretAccessKey: secretAccessKey,
|
||||||
|
credential.ConstCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
|
||||||
|
})
|
||||||
|
require.NoError(err)
|
||||||
|
request := &pb.ListHostsRequest{
|
||||||
|
Catalog: &hostcatalogs.HostCatalog{
|
||||||
|
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||||
|
Attributes: catalogAttrs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sets: []*hostsets.HostSet{{
|
||||||
|
Id: "hostset-0",
|
||||||
|
Attrs: &hostsets.HostSet_Attributes{Attributes: setAttrs},
|
||||||
|
}},
|
||||||
|
Persisted: &pb.HostCatalogPersisted{
|
||||||
|
Secrets: reqPersistedSecrets,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := p.ListHosts(ctx, request)
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(response.GetHosts(), 1)
|
||||||
|
hostResp := response.GetHosts()[0]
|
||||||
|
|
||||||
|
require.Equal(instanceID, hostResp.ExternalId)
|
||||||
|
require.ElementsMatch(hostResp.IpAddresses, tc.expectedIPs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-206
@@ -1,206 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
resource "random_id" "assume_role_name" {
|
|
||||||
prefix = "BoundaryPluginAssumeRole"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "assume_role" {
|
|
||||||
name = random_id.assume_role_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = {
|
|
||||||
AWS = data.aws_caller_identity.current.arn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Also trusted by the cross-role principal role so it can be used
|
|
||||||
# as the target in two-hop AssumeRole tests.
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = {
|
|
||||||
AWS = aws_iam_role.cross_role_principal.arn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "assume_role_ec2_describeinstances" {
|
|
||||||
role = aws_iam_role.assume_role.name
|
|
||||||
policy_arn = aws_iam_policy.ec2_describeinstances.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
output "assume_role_arn" {
|
|
||||||
value = aws_iam_role.assume_role.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Error case: role exists with ec2:DescribeInstances but trust policy
|
|
||||||
# does not allow the caller to assume it.
|
|
||||||
resource "random_id" "assume_role_no_trust_name" {
|
|
||||||
prefix = "BoundaryPluginNoTrust"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "assume_role_no_trust" {
|
|
||||||
name = random_id.assume_role_no_trust_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Deny"
|
|
||||||
Principal = {
|
|
||||||
AWS = data.aws_caller_identity.current.arn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output "assume_role_no_trust_arn" {
|
|
||||||
value = aws_iam_role.assume_role_no_trust.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Error case: role exists and the caller can assume it, but has no
|
|
||||||
# ec2:DescribeInstances permission.
|
|
||||||
# Also trusted by cross_role_principal so this same role can be reused as the
|
|
||||||
# no-EC2-permission target in two-hop (cross-role) error case tests.
|
|
||||||
resource "random_id" "assume_role_no_ec2_permission_name" {
|
|
||||||
prefix = "BoundaryPluginNoEC2Perm"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "assume_role_no_ec2_permission" {
|
|
||||||
name = random_id.assume_role_no_ec2_permission_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = {
|
|
||||||
AWS = data.aws_caller_identity.current.arn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
# Also trusted by the cross-role principal so it can serve as the
|
|
||||||
# no-EC2-permission target in two-hop error case tests.
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = {
|
|
||||||
AWS = aws_iam_role.cross_role_principal.arn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output "assume_role_no_ec2_permission_arn" {
|
|
||||||
value = aws_iam_role.assume_role_no_ec2_permission.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Cross-role (two-hop) principal role.
|
|
||||||
# The test runner can assume this role. It has permission to AssumeRole into
|
|
||||||
# aws_iam_role.assume_role (the target role) but has no ec2:DescribeInstances
|
|
||||||
# of its own. This simulates the principal side of a two-hop role chain: the
|
|
||||||
# plugin must assume the principal first, then use those credentials to assume
|
|
||||||
# the target role before calling DescribeInstances.
|
|
||||||
# Note: both roles are in the same AWS account. This tests the plugin's
|
|
||||||
# two-hop credential chain logic without requiring a second account.
|
|
||||||
resource "random_id" "cross_role_principal_name" {
|
|
||||||
prefix = "BoundaryPluginCRPrincipal"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "cross_role_principal" {
|
|
||||||
name = random_id.cross_role_principal_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = {
|
|
||||||
AWS = data.aws_caller_identity.current.arn
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Single policy granting cross_role_principal permission to assume all target
|
|
||||||
# roles it needs for both happy path and error case tests.
|
|
||||||
resource "aws_iam_policy" "cross_role_principal_assume_targets" {
|
|
||||||
name = "${random_id.cross_role_principal_name.dec}-assume-targets"
|
|
||||||
|
|
||||||
policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Resource = [
|
|
||||||
aws_iam_role.assume_role.arn,
|
|
||||||
aws_iam_role.assume_role_no_ec2_permission.arn,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "cross_role_principal_assume_targets" {
|
|
||||||
role = aws_iam_role.cross_role_principal.name
|
|
||||||
policy_arn = aws_iam_policy.cross_role_principal_assume_targets.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
output "cross_role_principal_arn" {
|
|
||||||
value = aws_iam_role.cross_role_principal.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Error case: caller cannot assume the principal role (first hop denied).
|
|
||||||
# Reuses assume_role_no_trust — it already has a Deny trust policy for the
|
|
||||||
# caller, so it also serves as an unassumable principal in two-hop tests.
|
|
||||||
output "cross_role_principal_no_trust_arn" {
|
|
||||||
value = aws_iam_role.assume_role_no_trust.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Error case: principal can be assumed, but the target role's trust policy
|
|
||||||
# does not allow the principal to assume it (second hop denied).
|
|
||||||
# Reuses assume_role_no_trust as the target — its Deny trust policy blocks
|
|
||||||
# all callers including cross_role_principal.
|
|
||||||
output "cross_role_target_no_trust_arn" {
|
|
||||||
value = aws_iam_role.assume_role_no_trust.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Error case: both hops succeed but the target has no ec2:DescribeInstances.
|
|
||||||
# Reuses assume_role_no_ec2_permission — its trust policy allows
|
|
||||||
# cross_role_principal (see above), and it has no EC2 permission.
|
|
||||||
output "cross_role_target_no_ec2_permission_arn" {
|
|
||||||
value = aws_iam_role.assume_role_no_ec2_permission.arn
|
|
||||||
}
|
|
||||||
-322
@@ -1,322 +0,0 @@
|
|||||||
# Copyright IBM Corp. 2021, 2026
|
|
||||||
# SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
# Cross-account Dynamic Host Catalog test infrastructure.
|
|
||||||
#
|
|
||||||
# This file provisions the IAM resources needed to test two-hop AssumeRole
|
|
||||||
# across two separate AWS accounts:
|
|
||||||
#
|
|
||||||
# Account A (principal account) — the default provider
|
|
||||||
# * cross_account_principal: the plugin assumes this role first.
|
|
||||||
# It has sts:AssumeRole permission on the target role in Account B.
|
|
||||||
#
|
|
||||||
# Account B (target account) — the "target" provider alias
|
|
||||||
# * cross_account_target: holds ec2:DescribeInstances; its trust policy
|
|
||||||
# allows the Account A principal role to assume it. (happy path)
|
|
||||||
# * cross_account_target_no_ec2_permission: same trust policy but no
|
|
||||||
# ec2:DescribeInstances. (error case)
|
|
||||||
# * cross_account_target_no_trust: exists in Account B but its trust
|
|
||||||
# policy does not allow the Account A principal. (error case)
|
|
||||||
#
|
|
||||||
# Prerequisites:
|
|
||||||
# TARGET_AWS_ACCESS_KEY_ID, TARGET_AWS_SECRET_ACCESS_KEY, and (for temporary
|
|
||||||
# credentials) TARGET_AWS_SESSION_TOKEN must be set to credentials that have
|
|
||||||
# sufficient IAM permissions in Account B. The test runner skips this
|
|
||||||
# section when the access key ID or secret access key are absent.
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Provider for Account B (target account)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
provider "aws" {
|
|
||||||
alias = "target"
|
|
||||||
region = var.target_region
|
|
||||||
access_key = var.target_access_key_id
|
|
||||||
secret_key = var.target_secret_access_key
|
|
||||||
token = var.target_session_token
|
|
||||||
}
|
|
||||||
|
|
||||||
data "aws_caller_identity" "target" {
|
|
||||||
provider = aws.target
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Variables for Account B credentials and region
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
variable "target_access_key_id" {
|
|
||||||
description = "AWS access key ID for the target account (Account B)."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "target_secret_access_key" {
|
|
||||||
description = "AWS secret access key for the target account (Account B)."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "target_session_token" {
|
|
||||||
description = "AWS session token for the target account (Account B). Required when using temporary (STS) credentials."
|
|
||||||
type = string
|
|
||||||
sensitive = true
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
variable "target_region" {
|
|
||||||
description = "AWS region for the target account (Account B). Defaults to the same region as Account A."
|
|
||||||
type = string
|
|
||||||
default = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Account A: cross_account_principal role
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
resource "random_id" "cross_account_principal_name" {
|
|
||||||
prefix = "BoundaryPluginCAPrincipal"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "cross_account_principal" {
|
|
||||||
name = random_id.cross_account_principal_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = { AWS = data.aws_caller_identity.current.arn }
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Allow the principal role to assume the happy-path and no-EC2-permission
|
|
||||||
# target roles in Account B.
|
|
||||||
resource "aws_iam_policy" "cross_account_principal_assume_targets" {
|
|
||||||
name = "${random_id.cross_account_principal_name.dec}-assume-targets"
|
|
||||||
|
|
||||||
policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Resource = [
|
|
||||||
aws_iam_role.cross_account_target.arn,
|
|
||||||
aws_iam_role.cross_account_target_no_ec2_permission.arn,
|
|
||||||
]
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "cross_account_principal_assume_targets" {
|
|
||||||
role = aws_iam_role.cross_account_principal.name
|
|
||||||
policy_arn = aws_iam_policy.cross_account_principal_assume_targets.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
output "cross_account_principal_arn" {
|
|
||||||
value = aws_iam_role.cross_account_principal.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# Error case: caller cannot assume the principal role (first hop denied).
|
|
||||||
# Reuses assume_role_no_trust from iam_assume_role.tf — its Deny trust policy
|
|
||||||
# also blocks the test runner from assuming it as a principal.
|
|
||||||
output "cross_account_principal_no_trust_arn" {
|
|
||||||
value = aws_iam_role.assume_role_no_trust.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Account B: happy-path target role
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
resource "random_id" "cross_account_target_name" {
|
|
||||||
prefix = "BoundaryPluginCATarget"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "cross_account_target" {
|
|
||||||
provider = aws.target
|
|
||||||
name = random_id.cross_account_target_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = { AWS = aws_iam_role.cross_account_principal.arn }
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_policy" "cross_account_target_ec2_describeinstances" {
|
|
||||||
provider = aws.target
|
|
||||||
name = "${random_id.cross_account_target_name.dec}-ec2"
|
|
||||||
|
|
||||||
policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["ec2:DescribeInstances"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Resource = "*"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role_policy_attachment" "cross_account_target_ec2_describeinstances" {
|
|
||||||
provider = aws.target
|
|
||||||
role = aws_iam_role.cross_account_target.name
|
|
||||||
policy_arn = aws_iam_policy.cross_account_target_ec2_describeinstances.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
output "cross_account_target_arn" {
|
|
||||||
value = aws_iam_role.cross_account_target.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Account B: error case — both hops succeed but target has no ec2:DescribeInstances
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
resource "random_id" "cross_account_target_no_ec2_permission_name" {
|
|
||||||
prefix = "BoundaryPluginCATargetNoEC2"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "cross_account_target_no_ec2_permission" {
|
|
||||||
provider = aws.target
|
|
||||||
name = random_id.cross_account_target_no_ec2_permission_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Allow"
|
|
||||||
Principal = { AWS = aws_iam_role.cross_account_principal.arn }
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output "cross_account_target_no_ec2_permission_arn" {
|
|
||||||
value = aws_iam_role.cross_account_target_no_ec2_permission.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Account B: error case — principal cannot assume this target (second hop denied)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
resource "random_id" "cross_account_target_no_trust_name" {
|
|
||||||
prefix = "BoundaryPluginCATargetNoTrust"
|
|
||||||
byte_length = 4
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_iam_role" "cross_account_target_no_trust" {
|
|
||||||
provider = aws.target
|
|
||||||
name = random_id.cross_account_target_no_trust_name.dec
|
|
||||||
|
|
||||||
assume_role_policy = jsonencode({
|
|
||||||
Version = "2012-10-17"
|
|
||||||
Statement = [
|
|
||||||
{
|
|
||||||
Action = ["sts:AssumeRole", "sts:SetSourceIdentity"]
|
|
||||||
Effect = "Deny"
|
|
||||||
Principal = { AWS = aws_iam_role.cross_account_principal.arn }
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
tags = {
|
|
||||||
"boundary-demo" = local.hashicorp_email
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output "cross_account_target_no_trust_arn" {
|
|
||||||
value = aws_iam_role.cross_account_target_no_trust.arn
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Account B: EC2 instance for cross-account host discovery validation
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
data "aws_availability_zones" "target_azs" {
|
|
||||||
count = var.target_access_key_id != "" ? 1 : 0
|
|
||||||
provider = aws.target
|
|
||||||
}
|
|
||||||
|
|
||||||
data "aws_ami" "target_ubuntu" {
|
|
||||||
count = var.target_access_key_id != "" ? 1 : 0
|
|
||||||
provider = aws.target
|
|
||||||
most_recent = true
|
|
||||||
|
|
||||||
filter {
|
|
||||||
name = "name"
|
|
||||||
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
|
|
||||||
}
|
|
||||||
|
|
||||||
filter {
|
|
||||||
name = "virtualization-type"
|
|
||||||
values = ["hvm"]
|
|
||||||
}
|
|
||||||
|
|
||||||
owners = ["099720109477"] # Canonical
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_vpc" "target_vpc" {
|
|
||||||
count = var.target_access_key_id != "" ? 1 : 0
|
|
||||||
provider = aws.target
|
|
||||||
cidr_block = "10.1.0.0/16"
|
|
||||||
assign_generated_ipv6_cidr_block = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_subnet" "target_subnet" {
|
|
||||||
count = var.target_access_key_id != "" ? 1 : 0
|
|
||||||
provider = aws.target
|
|
||||||
vpc_id = aws_vpc.target_vpc[0].id
|
|
||||||
cidr_block = aws_vpc.target_vpc[0].cidr_block
|
|
||||||
ipv6_cidr_block = cidrsubnet(aws_vpc.target_vpc[0].ipv6_cidr_block, 8, 0)
|
|
||||||
availability_zone = data.aws_availability_zones.target_azs[0].names[0]
|
|
||||||
map_public_ip_on_launch = true
|
|
||||||
assign_ipv6_address_on_creation = true
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "aws_instance" "target_instances" {
|
|
||||||
count = var.target_access_key_id != "" ? length(local.instance_tags) : 0
|
|
||||||
provider = aws.target
|
|
||||||
ami = data.aws_ami.target_ubuntu[0].id
|
|
||||||
instance_type = "t3.nano"
|
|
||||||
subnet_id = aws_subnet.target_subnet[0].id
|
|
||||||
|
|
||||||
tags = local.instance_tags[count.index]
|
|
||||||
}
|
|
||||||
|
|
||||||
output "target_instance_ids" {
|
|
||||||
value = aws_instance.target_instances.*.id
|
|
||||||
}
|
|
||||||
|
|
||||||
output "target_instance_tags" {
|
|
||||||
value = {
|
|
||||||
for _, r in aws_instance.target_instances : r.id => r.tags
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+7
-29
@@ -39,10 +39,6 @@ type TestTerraformer struct {
|
|||||||
// The working directory for Terraform commands.
|
// The working directory for Terraform commands.
|
||||||
workDir string
|
workDir string
|
||||||
|
|
||||||
// vars holds optional key/value pairs passed as -var flags to every
|
|
||||||
// Terraform command that accepts them (apply, destroy, plan).
|
|
||||||
vars map[string]string
|
|
||||||
|
|
||||||
// The Terraform state. Use the Output method to look up particular
|
// The Terraform state. Use the Output method to look up particular
|
||||||
// outputs from the Terraform state.
|
// outputs from the Terraform state.
|
||||||
state *tfjson.State
|
state *tfjson.State
|
||||||
@@ -57,9 +53,8 @@ type TestTerraformer struct {
|
|||||||
|
|
||||||
// NewTestTerraformer initializes a Terraform directory. It just
|
// NewTestTerraformer initializes a Terraform directory. It just
|
||||||
// returns an initialized project, it does not perform any actions on
|
// returns an initialized project, it does not perform any actions on
|
||||||
// it. An optional vars map may be provided; each entry is passed as a
|
// it.
|
||||||
// -var flag to Terraform commands.
|
func NewTestTerraformer(workDir string) (*TestTerraformer, error) {
|
||||||
func NewTestTerraformer(workDir string, vars ...map[string]string) (*TestTerraformer, error) {
|
|
||||||
tfPath, err := exec.LookPath("terraform")
|
tfPath, err := exec.LookPath("terraform")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("LookPath error: %w", err)
|
return nil, fmt.Errorf("LookPath error: %w", err)
|
||||||
@@ -69,24 +64,10 @@ func NewTestTerraformer(workDir string, vars ...map[string]string) (*TestTerrafo
|
|||||||
return nil, errors.New("workDir is empty")
|
return nil, errors.New("workDir is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
tf := &TestTerraformer{
|
return &TestTerraformer{
|
||||||
tfPath: tfPath,
|
tfPath: tfPath,
|
||||||
workDir: workDir,
|
workDir: workDir,
|
||||||
}
|
}, nil
|
||||||
if len(vars) > 0 {
|
|
||||||
tf.vars = vars[0]
|
|
||||||
}
|
|
||||||
return tf, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// varArgs converts the stored vars map into a slice of -var flags
|
|
||||||
// suitable for appending to a Terraform command's argument list.
|
|
||||||
func (t *TestTerraformer) varArgs() []string {
|
|
||||||
args := make([]string, 0, len(t.vars)*2)
|
|
||||||
for k, v := range t.vars {
|
|
||||||
args = append(args, "-var", fmt.Sprintf("%s=%s", k, v))
|
|
||||||
}
|
|
||||||
return args
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deploy runs the following commands, in order: init, apply, plan,
|
// Deploy runs the following commands, in order: init, apply, plan,
|
||||||
@@ -233,8 +214,7 @@ func (t *TestTerraformer) init() error {
|
|||||||
|
|
||||||
// apply runs "terraform apply" in the project's working directory.
|
// apply runs "terraform apply" in the project's working directory.
|
||||||
func (t *TestTerraformer) apply() error {
|
func (t *TestTerraformer) apply() error {
|
||||||
args := append([]string{"apply", "-input=false", "-auto-approve"}, t.varArgs()...)
|
cmd := exec.Command(t.tfPath, "apply", "-input=false", "-auto-approve")
|
||||||
cmd := exec.Command(t.tfPath, args...)
|
|
||||||
cmd.Dir = t.workDir
|
cmd.Dir = t.workDir
|
||||||
stdoutStderr, err := cmd.CombinedOutput()
|
stdoutStderr, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -246,8 +226,7 @@ func (t *TestTerraformer) apply() error {
|
|||||||
|
|
||||||
// destroy runs "terraform destroy" in the project's working directory.
|
// destroy runs "terraform destroy" in the project's working directory.
|
||||||
func (t *TestTerraformer) destroy() error {
|
func (t *TestTerraformer) destroy() error {
|
||||||
args := append([]string{"destroy", "-input=false", "-auto-approve"}, t.varArgs()...)
|
cmd := exec.Command(t.tfPath, "destroy", "-input=false", "-auto-approve")
|
||||||
cmd := exec.Command(t.tfPath, args...)
|
|
||||||
cmd.Dir = t.workDir
|
cmd.Dir = t.workDir
|
||||||
stdoutStderr, err := cmd.CombinedOutput()
|
stdoutStderr, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -263,8 +242,7 @@ func (t *TestTerraformer) destroy() error {
|
|||||||
// it's run to get the data for for importing the state via
|
// it's run to get the data for for importing the state via
|
||||||
// terraform-json. The apply method ignores the plan generated here.
|
// terraform-json. The apply method ignores the plan generated here.
|
||||||
func (t *TestTerraformer) plan() error {
|
func (t *TestTerraformer) plan() error {
|
||||||
args := append([]string{"plan", "-input=false", "-out=plan.tfplan"}, t.varArgs()...)
|
cmd := exec.Command(t.tfPath, "plan", "-input=false", "-out=plan.tfplan")
|
||||||
cmd := exec.Command(t.tfPath, args...)
|
|
||||||
cmd.Dir = t.workDir
|
cmd.Dir = t.workDir
|
||||||
stdoutStderr, err := cmd.CombinedOutput()
|
stdoutStderr, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in new issue
Block a user