* feat(tests) Add Terraform testing to support Cross-account discovery **Problem** * To support cross-account DHC discovery, we need to add terraform configurations and tests to test against live AWS data. * However, there was nothing in place to support any AssumeRole testing, irrespective of cross-account. **Solution** * Added a new terraform file, iam_assume_role.tf, to contain the new terraform configurations for single-account AssumeRole and cross-role * Added a second terraform file, iam_cross_account.tf, to contain the new terraform configurations for cross-account * Refactored how the tests are organized. The previous static test cases used a system where a separate function was created for each test. This was moved to a dedicated file, e2e_host_static_test.go. * For AssumeRole tests (single account, cross role, and cross account), this system wasn't ideal since there are so many test cases and they are all extremely similar. A table driven system was better that passed in a set of variables to shared test helpers. A dedicated file, e2e_host_assumerole_test.go, was created to contain the testing infrastructure for AssumeRole tests. * e2e_host_test.go remains the main test runner that then calls e2e_host_assumerole_test.go and e2e_host_static_test.go * The terraform tests work by setting environment variables for the AWS account that you want to configure the terraform for. The existing setup worked for single-account and cross-role. However, to support cross-account, support had to be added for a second set of credentials. The second set of environment variables can now be set with the TARGET_ prefix. Some configuration in testing.go had to be added to support creating/destroying multiple terraform setups. **Testing** * The single account tests pass as expected. However, a bug was discovered during this process that will be addressed later. * The cross-role and cross-account tests failed as expected because this featured has not yet been delivered. * It was noticed that some of the existing static tests were flaky. The problem and fix were identified and will be addressed later.
397 lines
13 KiB
Go
397 lines
13 KiB
Go
// 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)
|
|
})
|
|
}
|
|
}
|