2026-01-16 15:56:19 -08:00
|
|
|
// Copyright IBM Corp. 2021, 2025
|
2023-01-03 22:03:42 +00:00
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
|
2021-09-21 15:04:49 -07:00
|
|
|
package testing
|
|
|
|
|
|
|
|
|
|
import (
|
2023-07-18 16:33:24 -07:00
|
|
|
"context"
|
2021-09-21 15:04:49 -07:00
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"sync"
|
2023-07-18 16:33:24 -07:00
|
|
|
"testing"
|
|
|
|
|
"time"
|
2021-09-21 15:04:49 -07:00
|
|
|
|
2023-09-22 12:26:08 -07:00
|
|
|
"github.com/aws/smithy-go"
|
2023-07-18 16:33:24 -07:00
|
|
|
"github.com/hashicorp/boundary-plugin-aws/internal/credential"
|
|
|
|
|
"github.com/hashicorp/boundary-plugin-aws/internal/values"
|
2023-09-22 12:26:08 -07:00
|
|
|
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
|
2021-09-21 15:04:49 -07:00
|
|
|
tfjson "github.com/hashicorp/terraform-json"
|
2023-07-18 16:33:24 -07:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
"google.golang.org/protobuf/types/known/structpb"
|
2021-09-21 15:04:49 -07:00
|
|
|
)
|
|
|
|
|
|
2023-07-18 16:33:24 -07:00
|
|
|
const expectedIamUserCount = 6
|
|
|
|
|
|
2021-09-21 15:04:49 -07:00
|
|
|
// TestTerraformer is an object representing the lifecycle of a
|
|
|
|
|
// particular Terraform project.
|
|
|
|
|
//
|
|
|
|
|
// Note that Terraform must be installed in the system PATH (not to
|
|
|
|
|
// be confused with the working directory below). This object does
|
|
|
|
|
// not manage anything with regards to installation or version
|
|
|
|
|
// management.
|
|
|
|
|
type TestTerraformer struct {
|
|
|
|
|
// The full path to the Terraform binary, set during initialization
|
|
|
|
|
// to the binary found in the system path.
|
|
|
|
|
tfPath string
|
|
|
|
|
|
|
|
|
|
// The working directory for Terraform commands.
|
|
|
|
|
workDir string
|
|
|
|
|
|
|
|
|
|
// The Terraform state. Use the Output method to look up particular
|
|
|
|
|
// outputs from the Terraform state.
|
|
|
|
|
state *tfjson.State
|
|
|
|
|
|
|
|
|
|
// Keeps track of whether or not we have run the init command
|
|
|
|
|
// during this run of Terraform. This only needs to be done once.
|
|
|
|
|
// Do not use initErr directly; it stores the init error if one was
|
|
|
|
|
// encountered on init.
|
|
|
|
|
initOnce sync.Once
|
|
|
|
|
initErr error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewTestTerraformer initializes a Terraform directory. It just
|
|
|
|
|
// returns an initialized project, it does not perform any actions on
|
|
|
|
|
// it.
|
|
|
|
|
func NewTestTerraformer(workDir string) (*TestTerraformer, error) {
|
|
|
|
|
tfPath, err := exec.LookPath("terraform")
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("LookPath error: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if workDir == "" {
|
|
|
|
|
return nil, errors.New("workDir is empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &TestTerraformer{
|
|
|
|
|
tfPath: tfPath,
|
|
|
|
|
workDir: workDir,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deploy runs the following commands, in order: init, apply, plan,
|
|
|
|
|
// json. The output of the JSON command is then imported to the
|
|
|
|
|
// internal state.
|
|
|
|
|
func (t *TestTerraformer) Deploy() error {
|
|
|
|
|
if err := t.init(); err != nil {
|
|
|
|
|
return fmt.Errorf("error running Terraform init: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := t.apply(); err != nil {
|
|
|
|
|
return fmt.Errorf("error running Terraform apply: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := t.plan(); err != nil {
|
|
|
|
|
return fmt.Errorf("error running Terraform plan: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output, err := t.showJson()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("error getting Terraform plan JSON data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
newState := new(tfjson.Plan)
|
|
|
|
|
if err := json.Unmarshal(output, newState); err != nil {
|
|
|
|
|
return fmt.Errorf("error reading new Terraform plan JSON data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.state = newState.PriorState
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Destroy runs the following commands, in order: init, destroy. The
|
|
|
|
|
// state is also cleared.
|
|
|
|
|
func (t *TestTerraformer) Destroy() error {
|
|
|
|
|
if err := t.init(); err != nil {
|
|
|
|
|
return fmt.Errorf("error running Terraform init: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := t.destroy(); err != nil {
|
|
|
|
|
return fmt.Errorf("error running Terraform destroy: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t.state = nil
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetOutput returns the value of the output located at key.
|
|
|
|
|
//
|
|
|
|
|
// Outputs for the root module are supported only. It's an error to
|
|
|
|
|
// run this when no state exists.
|
2023-03-20 11:02:17 -07:00
|
|
|
func (t *TestTerraformer) GetOutput(key string) (any, error) {
|
2021-09-21 15:04:49 -07:00
|
|
|
if key == "" {
|
|
|
|
|
return nil, errors.New("key must not be empty")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.state == nil {
|
|
|
|
|
return nil, errors.New("state is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.state.Values == nil {
|
|
|
|
|
return nil, errors.New("state integrity error: no state values")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.state.Values.Outputs == nil {
|
|
|
|
|
return nil, errors.New("state integrity error: outputs is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
output, ok := t.state.Values.Outputs[key]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("output with key %q not found", key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if output == nil {
|
|
|
|
|
return nil, fmt.Errorf("state integrity error: output at key %q is nil", key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return output.Value, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-22 17:00:37 -07:00
|
|
|
// GetOutputString wraps GetOutput and returns the value as a string. It's an
|
|
|
|
|
// error if the return value is actually not a string.
|
|
|
|
|
func (t *TestTerraformer) GetOutputString(key string) (string, error) {
|
|
|
|
|
value, err := t.GetOutput(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
str, ok := value.(string)
|
|
|
|
|
if !ok {
|
|
|
|
|
return "", fmt.Errorf("expected output value at key %q to be string, got %T", key, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return str, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetOutputMap wraps GetOutput and returns the value as a map. It's an
|
|
|
|
|
// error if the return value is actually not a map.
|
2023-03-20 11:02:17 -07:00
|
|
|
func (t *TestTerraformer) GetOutputMap(key string) (map[string]any, error) {
|
2021-09-22 17:00:37 -07:00
|
|
|
value, err := t.GetOutput(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-20 11:02:17 -07:00
|
|
|
m, ok := value.(map[string]any)
|
2021-09-22 17:00:37 -07:00
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("expected output value at key %q to be map, got %T", key, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return m, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetOutputSlice wraps GetOutput and returns the value as a slice. It's an
|
|
|
|
|
// error if the return value is actually not a slice.
|
2023-03-20 11:02:17 -07:00
|
|
|
func (t *TestTerraformer) GetOutputSlice(key string) ([]any, error) {
|
2021-09-22 17:00:37 -07:00
|
|
|
value, err := t.GetOutput(key)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-20 11:02:17 -07:00
|
|
|
m, ok := value.([]any)
|
2021-09-22 17:00:37 -07:00
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("expected output value at key %q to be slice, got %T", key, value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return m, nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-09-21 15:04:49 -07:00
|
|
|
// init runs "terraform init" in the project's working directory.
|
|
|
|
|
//
|
|
|
|
|
// This is only ever done once.
|
|
|
|
|
func (t *TestTerraformer) init() error {
|
|
|
|
|
t.initOnce.Do(func() {
|
|
|
|
|
cmd := exec.Command(t.tfPath, "init")
|
|
|
|
|
cmd.Dir = t.workDir
|
|
|
|
|
stdoutStderr, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.initErr = fmt.Errorf("%s\n%s", stdoutStderr, err)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return t.initErr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// apply runs "terraform apply" in the project's working directory.
|
|
|
|
|
func (t *TestTerraformer) apply() error {
|
|
|
|
|
cmd := exec.Command(t.tfPath, "apply", "-input=false", "-auto-approve")
|
|
|
|
|
cmd.Dir = t.workDir
|
|
|
|
|
stdoutStderr, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("%s\n%s", stdoutStderr, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// destroy runs "terraform destroy" in the project's working directory.
|
|
|
|
|
func (t *TestTerraformer) destroy() error {
|
|
|
|
|
cmd := exec.Command(t.tfPath, "destroy", "-input=false", "-auto-approve")
|
|
|
|
|
cmd.Dir = t.workDir
|
|
|
|
|
stdoutStderr, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("%s\n%s", stdoutStderr, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// plan runs "terraform plan" in the project's working directory.
|
|
|
|
|
//
|
|
|
|
|
// Note that plan is not run as part of the apply workflow; rather,
|
|
|
|
|
// it's run to get the data for for importing the state via
|
|
|
|
|
// terraform-json. The apply method ignores the plan generated here.
|
|
|
|
|
func (t *TestTerraformer) plan() error {
|
|
|
|
|
cmd := exec.Command(t.tfPath, "plan", "-input=false", "-out=plan.tfplan")
|
|
|
|
|
cmd.Dir = t.workDir
|
|
|
|
|
stdoutStderr, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("%s\n%s", stdoutStderr, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// showJson runs "terraform show -json". The output is returned along
|
|
|
|
|
// with any error.
|
|
|
|
|
//
|
|
|
|
|
// Output is only the results of stdout and should be suitable for
|
|
|
|
|
// parsing.
|
|
|
|
|
//
|
|
|
|
|
// This method expects the plan.tfplan file to be generated in the
|
|
|
|
|
// working directory already. This can be done with the plan method.
|
|
|
|
|
func (t *TestTerraformer) showJson() ([]byte, error) {
|
|
|
|
|
cmd := exec.Command(t.tfPath, "show", "-json", "plan.tfplan")
|
|
|
|
|
cmd.Dir = t.workDir
|
|
|
|
|
output, err := cmd.Output()
|
|
|
|
|
if err != nil {
|
|
|
|
|
exitErr, ok := err.(*exec.ExitError)
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("%s\n%s", exitErr.Stderr, exitErr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return output, nil
|
|
|
|
|
}
|
2023-07-18 16:33:24 -07:00
|
|
|
|
|
|
|
|
func requireCredentialsInvalid(t *testing.T, accessKeyId, secretAccessKey string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
c, err := awsutil.NewCredentialsConfig(
|
|
|
|
|
awsutil.WithAccessKey(accessKeyId),
|
|
|
|
|
awsutil.WithSecretKey(secretAccessKey),
|
|
|
|
|
)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
|
|
// We need to wait for invalidation as while awsutil waits for
|
|
|
|
|
// credential creation, deletion of the old credentials returns
|
|
|
|
|
// immediately.
|
|
|
|
|
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second*30)
|
|
|
|
|
defer cancel()
|
|
|
|
|
waitErr:
|
|
|
|
|
for {
|
2023-09-22 12:26:08 -07:00
|
|
|
_, err = c.GetCallerIdentity(context.Background())
|
2023-07-18 16:33:24 -07:00
|
|
|
if err != nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
select {
|
|
|
|
|
case <-time.After(time.Second):
|
|
|
|
|
// pass
|
|
|
|
|
|
|
|
|
|
case <-timeoutCtx.Done():
|
|
|
|
|
break waitErr
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
require.NotNil(err)
|
2023-09-22 12:26:08 -07:00
|
|
|
var ae smithy.APIError
|
|
|
|
|
require.True(errors.As(err, &ae))
|
|
|
|
|
require.Equal("InvalidClientTokenId", ae.ErrorCode())
|
2023-07-18 16:33:24 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requireCredentialsValid(t *testing.T, accessKeyId, secretAccessKey string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
c, err := awsutil.NewCredentialsConfig(
|
|
|
|
|
awsutil.WithAccessKey(accessKeyId),
|
|
|
|
|
awsutil.WithSecretKey(secretAccessKey),
|
|
|
|
|
)
|
|
|
|
|
require.NoError(err)
|
2023-09-22 12:26:08 -07:00
|
|
|
_, err = c.GetCallerIdentity(context.Background())
|
2023-07-18 16:33:24 -07:00
|
|
|
require.NoError(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validatePersistedSecrets(t *testing.T, persistedSecrets *structpb.Struct, accessKeyId, secretAccessKey string, rotate bool) (string, string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
require.NotNil(persistedSecrets)
|
|
|
|
|
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
require.NotZero(persistedAccessKeyId)
|
|
|
|
|
if rotate {
|
|
|
|
|
require.NotEqual(accessKeyId, persistedAccessKeyId)
|
|
|
|
|
} else {
|
|
|
|
|
require.Equal(accessKeyId, persistedAccessKeyId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
require.NotZero(persistedSecretAccessKey)
|
|
|
|
|
if rotate {
|
|
|
|
|
require.NotEqual(secretAccessKey, persistedSecretAccessKey)
|
|
|
|
|
} else {
|
|
|
|
|
require.Equal(secretAccessKey, persistedSecretAccessKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
if rotate {
|
|
|
|
|
require.NotZero(persistedCredsLastRotatedTime)
|
|
|
|
|
requireCredentialsInvalid(t, accessKeyId, secretAccessKey)
|
|
|
|
|
} else {
|
|
|
|
|
require.Zero(persistedCredsLastRotatedTime)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return persistedAccessKeyId, persistedSecretAccessKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validateUpdateSecrets(
|
|
|
|
|
t *testing.T, persistedSecrets *structpb.Struct, currentCredsLastRotatedTime time.Time,
|
|
|
|
|
currentAccessKeyId, currentSecretAccessKey, newAccessKeyId, newSecretAccessKey string,
|
|
|
|
|
rotated, rotate bool) (string, string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
|
|
// Complex checks based on the scenarios.
|
|
|
|
|
persistedAccessKeyId, err := values.GetStringValue(persistedSecrets, credential.ConstAccessKeyId, true)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
require.NotZero(persistedAccessKeyId)
|
|
|
|
|
persistedSecretAccessKey, err := values.GetStringValue(persistedSecrets, credential.ConstSecretAccessKey, true)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
require.NotZero(persistedSecretAccessKey)
|
|
|
|
|
persistedCredsLastRotatedTime, err := values.GetTimeValue(persistedSecrets, credential.ConstCredsLastRotatedTime)
|
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
|
|
// Our test scenarios are complex due the multi-dimensional nature
|
|
|
|
|
// of criteria, so we lay them out in a switch below.
|
|
|
|
|
switch {
|
|
|
|
|
case newAccessKeyId != "" && rotated && rotate:
|
|
|
|
|
// The new access key ID was provided, we had previously rotated
|
|
|
|
|
// the credentials before, and the new credential set is to be
|
|
|
|
|
// rotated as well. In this case, the old credentials should have
|
|
|
|
|
// been deleted, and the new credentials should have been rotated,
|
|
|
|
|
// hence, should not match the new credentials initially
|
|
|
|
|
// provided. Rotation time should be non-zero and updated.
|
|
|
|
|
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
|
|
|
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
|
|
|
|
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
|
|
|
|
require.NotZero(persistedCredsLastRotatedTime)
|
|
|
|
|
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
|
|
|
|
|
|
|
|
|
case newAccessKeyId != "" && rotated && !rotate:
|
|
|
|
|
// The new access key ID was provided, we had previously rotated
|
|
|
|
|
// the credentials before, and the new credential is *not*
|
|
|
|
|
// rotated. In this case, the old credentials should have
|
|
|
|
|
// been deleted, But the new credentials should have not been
|
|
|
|
|
// rotated, and hence should be the same. Rotation time should be
|
|
|
|
|
// zero.
|
|
|
|
|
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
|
|
|
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
|
|
|
|
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
|
|
|
|
require.Zero(persistedCredsLastRotatedTime)
|
|
|
|
|
|
|
|
|
|
case newAccessKeyId != "" && !rotated && rotate:
|
|
|
|
|
// The new access key ID was provided, we *have not* previously
|
|
|
|
|
// rotated the credentials, and the new credential set is to be
|
|
|
|
|
// rotated. In this case, the old credentials should have been
|
|
|
|
|
// left alone, and the new credentials should have been rotated,
|
|
|
|
|
// hence, should not match the new credentials initially
|
|
|
|
|
// provided. Rotation time should be non-zero, but updated.
|
|
|
|
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
|
|
|
require.NotEqual(persistedAccessKeyId, newAccessKeyId)
|
|
|
|
|
require.NotEqual(persistedSecretAccessKey, newSecretAccessKey)
|
|
|
|
|
require.NotZero(persistedCredsLastRotatedTime)
|
|
|
|
|
require.True(persistedCredsLastRotatedTime.After(currentCredsLastRotatedTime))
|
|
|
|
|
|
|
|
|
|
case newAccessKeyId != "" && !rotated && !rotate:
|
|
|
|
|
// The new access key ID was provided, but we have not rotated
|
|
|
|
|
// the credentials previously and we still don't plan on rotating
|
|
|
|
|
// them. In this case, the old credentials should still be valid,
|
|
|
|
|
// and the persisted ones should match the new ones provided.
|
|
|
|
|
// Rotation time should be zero.
|
|
|
|
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
|
|
|
require.Equal(persistedAccessKeyId, newAccessKeyId)
|
|
|
|
|
require.Equal(persistedSecretAccessKey, newSecretAccessKey)
|
|
|
|
|
require.Zero(persistedCredsLastRotatedTime)
|
|
|
|
|
|
|
|
|
|
case newAccessKeyId == "" && rotated && rotate:
|
|
|
|
|
// No new credentials have been provided, but we have previously
|
|
|
|
|
// rotated and are still rotating credentials. This is a no-op.
|
|
|
|
|
// Existing credentials should still be valid and match the ones
|
|
|
|
|
// persisted to state. Rotation time should be identical since
|
|
|
|
|
// no new rotation occurred.
|
|
|
|
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
|
|
|
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
|
|
|
|
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
|
|
|
|
require.NotZero(persistedCredsLastRotatedTime)
|
|
|
|
|
require.True(currentCredsLastRotatedTime.Equal(persistedCredsLastRotatedTime))
|
|
|
|
|
|
|
|
|
|
case newAccessKeyId == "" && rotated && !rotate:
|
|
|
|
|
// No new credentials have been provided, and we have previously
|
|
|
|
|
// rotated the credentials. This is actually an error, but we
|
|
|
|
|
// don't test it here; it's covered in unit testing (see
|
|
|
|
|
// plugin_test.go).
|
|
|
|
|
require.FailNow("testing rotated-to-not-rotated scenario not implemented by this helper")
|
|
|
|
|
|
|
|
|
|
case newAccessKeyId == "" && !rotated && rotate:
|
|
|
|
|
// No new credentials have been provided, and while we did not
|
|
|
|
|
// rotate before, we want to switch to rotation. In this case,
|
|
|
|
|
// the existing persisted credentials should have been rotated,
|
|
|
|
|
// with a new non-zero timestamp.
|
|
|
|
|
requireCredentialsInvalid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
|
|
|
require.NotEqual(persistedAccessKeyId, currentAccessKeyId)
|
|
|
|
|
require.NotEqual(persistedSecretAccessKey, currentSecretAccessKey)
|
|
|
|
|
require.NotZero(persistedCredsLastRotatedTime)
|
|
|
|
|
|
|
|
|
|
case newAccessKeyId == "" && !rotated && !rotate:
|
|
|
|
|
// No new credentials have been provided and we have not, nor do
|
|
|
|
|
// not, plan on rotating the credentials. This is a no-op.
|
|
|
|
|
// Existing credentials should still be valid and match the ones
|
|
|
|
|
// persisted to state. Rotation time should remain at zero.
|
|
|
|
|
requireCredentialsValid(t, currentAccessKeyId, currentSecretAccessKey)
|
|
|
|
|
require.Equal(persistedAccessKeyId, currentAccessKeyId)
|
|
|
|
|
require.Equal(persistedSecretAccessKey, currentSecretAccessKey)
|
|
|
|
|
require.Zero(persistedCredsLastRotatedTime)
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
// Scenario was reached that was not covered by this function.
|
|
|
|
|
require.FailNow("unknown test scenario")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return persistedAccessKeyId, persistedSecretAccessKey
|
|
|
|
|
}
|