// 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) }) } }