WIP - more unit testing

This commit is contained in:
Chris Marchesi committed 2021-09-30 17:05:30 -07:00
1 parent cb00f0cb7a
commit f7c5709046
4 files changed
+299 -175

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
.terraform*
terraform.tfstate*
plan.tfplan
*.log
-175
View File
@@ -9,11 +9,8 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -455,178 +452,6 @@ func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*p
}, nil
}
type awsCatalogPersistedState struct {
AccessKeyId string
SecretAccessKey string
CredsLastRotatedTime time.Time
}
func awsCatalogPersistedStateFromProto(in *pb.HostCatalogPersisted) (*awsCatalogPersistedState, error) {
data := in.GetSecrets()
if data == nil {
return nil, errors.New("missing persisted secrets")
}
accessKeyId, err := getStringValue(data, constAccessKeyId, true)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
secretAccessKey, err := getStringValue(data, constSecretAccessKey, true)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
credsLastRotatedTime, err := getTimeValue(data, constCredsLastRotatedTime)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
return &awsCatalogPersistedState{
AccessKeyId: accessKeyId,
SecretAccessKey: secretAccessKey,
CredsLastRotatedTime: credsLastRotatedTime,
}, nil
}
func (s *awsCatalogPersistedState) ToProto() (*pb.HostCatalogPersisted, error) {
data, err := structpb.NewStruct(map[string]interface{}{
constAccessKeyId: s.AccessKeyId,
constSecretAccessKey: s.SecretAccessKey,
constCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
})
if err != nil {
return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err)
}
return &pb.HostCatalogPersisted{Secrets: data}, nil
}
func (s *awsCatalogPersistedState) ValidateCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if _, err := c.GetCallerIdentity(); err != nil {
return fmt.Errorf("error validating credentials: %w", err)
}
return nil
}
func (s *awsCatalogPersistedState) RotateCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if err := c.RotateKeys(awsutil.WithValidityCheckTimeout(rotationWaitTimeout)); err != nil {
return fmt.Errorf("error rotating credentials: %w", err)
}
s.AccessKeyId = c.AccessKey
s.SecretAccessKey = c.SecretKey
s.CredsLastRotatedTime = time.Now()
return nil
}
// ReplaceCreds replaces the access key in the state with a new key.
// If the existing key was rotated at any point in time, it is
// deleted first, otherwise it's left alone.
func (s *awsCatalogPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
if accessKeyId == "" {
return errors.New("access key id cannot be empty")
}
if secretAccessKey == "" {
return errors.New("secret access key cannot be empty")
}
if accessKeyId == s.AccessKeyId {
return errors.New("attempting to replace access key with the same one")
}
if !s.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
// it.
if err := s.DeleteCreds(); err != nil {
return err
}
}
// Set the new attributes and clear the rotated time.
s.AccessKeyId = accessKeyId
s.SecretAccessKey = secretAccessKey
s.CredsLastRotatedTime = time.Time{}
return nil
}
// DeleteCreds deletes the credentials in the state. The access key
// ID, secret access key, and rotation time fields are zeroed out in
// the state just to ensure that they cannot be re-used after.
func (s *awsCatalogPersistedState) DeleteCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if err := c.DeleteAccessKey(s.AccessKeyId); err != nil {
// Determine if the deletion error was due to a missing
// resource. If it was, just pass it.
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == iam.ErrCodeNoSuchEntityException {
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredsLastRotatedTime = time.Time{}
return nil
}
// Otherwise treat it as an actual error.
return fmt.Errorf("error deleting old access key: %w", err)
}
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredsLastRotatedTime = time.Time{}
return nil
}
// GetSession returns a configured AWS session for the credentials in
// the state.
func (s *awsCatalogPersistedState) GetSession() (*session.Session, error) {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return nil, err
}
return c.GetSession()
}
// EC2Client returns a configured EC2 client based on the session
// information stored in the state.
func (s *awsCatalogPersistedState) EC2Client(region string) (*ec2.EC2, error) {
sess, err := s.GetSession()
if err != nil {
return nil, fmt.Errorf("error getting AWS session: %w", err)
}
return ec2.New(sess, aws.NewConfig().WithRegion(region)), nil
}
func getStringValue(in *structpb.Struct, k string, required bool) (string, error) {
mv := in.AsMap()
v, ok := mv[k]
+188
View File
@@ -0,0 +1,188 @@
package plugin
import (
"errors"
"fmt"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/iam"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"google.golang.org/protobuf/types/known/structpb"
)
type awsCatalogPersistedState struct {
AccessKeyId string
SecretAccessKey string
CredsLastRotatedTime time.Time
}
func awsCatalogPersistedStateFromProto(in *pb.HostCatalogPersisted) (*awsCatalogPersistedState, error) {
data := in.GetSecrets()
if data == nil {
return nil, errors.New("missing persisted secrets")
}
accessKeyId, err := getStringValue(data, constAccessKeyId, true)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
secretAccessKey, err := getStringValue(data, constSecretAccessKey, true)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
credsLastRotatedTime, err := getTimeValue(data, constCredsLastRotatedTime)
if err != nil {
return nil, fmt.Errorf("persisted state integrity error: %w", err)
}
return &awsCatalogPersistedState{
AccessKeyId: accessKeyId,
SecretAccessKey: secretAccessKey,
CredsLastRotatedTime: credsLastRotatedTime,
}, nil
}
func (s *awsCatalogPersistedState) ToProto() (*pb.HostCatalogPersisted, error) {
data, err := structpb.NewStruct(map[string]interface{}{
constAccessKeyId: s.AccessKeyId,
constSecretAccessKey: s.SecretAccessKey,
constCredsLastRotatedTime: s.CredsLastRotatedTime.Format(time.RFC3339Nano),
})
if err != nil {
return nil, fmt.Errorf("error converting state to structpb.Struct: %w", err)
}
return &pb.HostCatalogPersisted{Secrets: data}, nil
}
func (s *awsCatalogPersistedState) ValidateCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if _, err := c.GetCallerIdentity(); err != nil {
return fmt.Errorf("error validating credentials: %w", err)
}
return nil
}
func (s *awsCatalogPersistedState) RotateCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if err := c.RotateKeys(awsutil.WithValidityCheckTimeout(rotationWaitTimeout)); err != nil {
return fmt.Errorf("error rotating credentials: %w", err)
}
s.AccessKeyId = c.AccessKey
s.SecretAccessKey = c.SecretKey
s.CredsLastRotatedTime = time.Now()
return nil
}
// ReplaceCreds replaces the access key in the state with a new key.
// If the existing key was rotated at any point in time, it is
// deleted first, otherwise it's left alone.
func (s *awsCatalogPersistedState) ReplaceCreds(accessKeyId, secretAccessKey string) error {
if accessKeyId == "" {
return errors.New("access key id cannot be empty")
}
if secretAccessKey == "" {
return errors.New("secret access key cannot be empty")
}
if accessKeyId == s.AccessKeyId {
return errors.New("attempting to replace access key with the same one")
}
if !s.CredsLastRotatedTime.IsZero() {
// Delete old/existing credentials. This is done with the same
// credentials to ensure that it has the proper permissions to do
// it.
if err := s.DeleteCreds(); err != nil {
return err
}
}
// Set the new attributes and clear the rotated time.
s.AccessKeyId = accessKeyId
s.SecretAccessKey = secretAccessKey
s.CredsLastRotatedTime = time.Time{}
return nil
}
// DeleteCreds deletes the credentials in the state. The access key
// ID, secret access key, and rotation time fields are zeroed out in
// the state just to ensure that they cannot be re-used after.
func (s *awsCatalogPersistedState) DeleteCreds() error {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return fmt.Errorf("error loading credentials: %w", err)
}
if err := c.DeleteAccessKey(s.AccessKeyId); err != nil {
// Determine if the deletion error was due to a missing
// resource. If it was, just pass it.
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == iam.ErrCodeNoSuchEntityException {
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredsLastRotatedTime = time.Time{}
return nil
}
// Otherwise treat it as an actual error.
return fmt.Errorf("error deleting old access key: %w", err)
}
s.AccessKeyId = ""
s.SecretAccessKey = ""
s.CredsLastRotatedTime = time.Time{}
return nil
}
// GetSession returns a configured AWS session for the credentials in
// the state.
func (s *awsCatalogPersistedState) GetSession() (*session.Session, error) {
c, err := awsutil.NewCredentialsConfig(
awsutil.WithAccessKey(s.AccessKeyId),
awsutil.WithSecretKey(s.SecretAccessKey),
)
if err != nil {
return nil, err
}
return c.GetSession()
}
// EC2Client returns a configured EC2 client based on the session
// information stored in the state.
func (s *awsCatalogPersistedState) EC2Client(region string) (*ec2.EC2, error) {
sess, err := s.GetSession()
if err != nil {
return nil, fmt.Errorf("error getting AWS session: %w", err)
}
return ec2.New(sess, aws.NewConfig().WithRegion(region)), nil
}
+110
View File
@@ -0,0 +1,110 @@
package plugin
import (
"testing"
"time"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/require"
)
func TestAwsCatalogPersistedStateFromProto(t *testing.T) {
cases := []struct {
name string
in *pb.HostCatalogPersisted
expected *awsCatalogPersistedState
expectedErr string
}{
{
name: "no secrets",
in: &pb.HostCatalogPersisted{
Secrets: nil,
},
expectedErr: "missing persisted secrets",
},
{
name: "missing access key ID",
in: &pb.HostCatalogPersisted{
// Nil should be the same as empty map here
Secrets: mustStruct(nil),
},
expectedErr: "persisted state integrity error: missing required value \"access_key_id\"",
},
{
name: "missing secret access key",
in: &pb.HostCatalogPersisted{
// Nil should be the same as empty map here
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
}),
},
expectedErr: "persisted state integrity error: missing required value \"secret_access_key\"",
},
{
name: "bad last rotated time",
in: &pb.HostCatalogPersisted{
// Nil should be the same as empty map here
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "notatime",
}),
},
expectedErr: "persisted state integrity error: could not parse time in value \"creds_last_rotated_time\": parsing time \"notatime\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"notatime\" as \"2006\"",
},
{
name: "good with non-zero timestamp",
in: &pb.HostCatalogPersisted{
// Nil should be the same as empty map here
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: "2006-01-02T15:04:05+07:00",
}),
},
expected: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredsLastRotatedTime: func() time.Time {
t, err := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
if err != nil {
panic(err)
}
return t
}(),
},
},
{
name: "good with zero timestamp",
in: &pb.HostCatalogPersisted{
// Nil should be the same as empty map here
Secrets: mustStruct(map[string]interface{}{
constAccessKeyId: "foobar",
constSecretAccessKey: "bazqux",
constCredsLastRotatedTime: (time.Time{}).Format(time.RFC3339Nano),
}),
},
expected: &awsCatalogPersistedState{
AccessKeyId: "foobar",
SecretAccessKey: "bazqux",
CredsLastRotatedTime: time.Time{},
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := awsCatalogPersistedStateFromProto(tc.in)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}