305 lines
10 KiB
Go
305 lines
10 KiB
Go
// 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))
|
|
})
|
|
}
|
|
}
|
|
|