feat: return credential state for all status errors

This commit is contained in:
Damian Debkowski authored and Danielle Miu committed 2024-07-17 10:54:18 -04:00
1 parent a0cb3b8fee
commit 08d1e99e8b
16 files changed
+1450 -623

No files matched your search

+12 -6
View File
@@ -34,8 +34,9 @@ type CredentialAttributes struct {
}
// GetCredentialsConfig parses values out of a protobuf struct secrets and returns a
// CredentialsConfig used for configuring an AWS session. An error is returned if
// any unrecognized fields are found in the protobuf struct input.
// CredentialsConfig used for configuring an AWS session. An status error is returned
// with an InvalidArgument code if any unrecognized fields are found in the protobuf
// struct input.
func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes, required bool) (*awsutil.CredentialsConfig, error) {
// initialize secrets if it is nil
// secrets can be nil because static credentials are optional
@@ -116,13 +117,18 @@ func GetCredentialsConfig(secrets *structpb.Struct, attrs *CredentialAttributes,
opts = append(opts, awsutil.WithRoleTags(attrs.RoleTags))
}
return awsutil.NewCredentialsConfig(opts...)
cfg, err := awsutil.NewCredentialsConfig(opts...)
if err != nil {
return nil, errors.BadRequestStatus(err.Error())
}
return cfg, nil
}
// GetCredentialAttributes parses values out of a protobuf struct input and returns a
// CredentialAttributes used for configuring an AWS session. An error is returned if
// any of the following fields are missing from the protobuf struct input or have
// invalid value types: region, disableCredentialRotation
// CredentialAttributes used for configuring an AWS session. An status error is returned
// with an InvalidArgument code if any of the following fields are missing from the protobuf
// struct input or have invalid value types: region, disableCredentialRotation
func GetCredentialAttributes(in *structpb.Struct) (*CredentialAttributes, error) {
badFields := make(map[string]string)
+25 -18
View File
@@ -5,13 +5,14 @@ package credential
import (
"context"
"errors"
stderr "errors"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/hashicorp/boundary-plugin-aws/internal/errors"
"github.com/hashicorp/boundary-plugin-aws/internal/values"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"google.golang.org/protobuf/types/known/structpb"
@@ -71,7 +72,7 @@ func WithStateTestOpts(opts []awsutil.Option) AwsCredentialPersistedStateOption
func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if s.CredentialsConfig != nil {
return errors.New("credentials config already set")
return stderr.New("credentials config already set")
}
s.CredentialsConfig = x
@@ -83,7 +84,7 @@ func WithCredentialsConfig(x *awsutil.CredentialsConfig) AwsCredentialPersistedS
func WithCredsLastRotatedTime(t time.Time) AwsCredentialPersistedStateOption {
return func(s *AwsCredentialPersistedState) error {
if !s.CredsLastRotatedTime.IsZero() {
return errors.New("last rotation time already set")
return stderr.New("last rotation time already set")
}
s.CredsLastRotatedTime = t
@@ -106,13 +107,15 @@ func NewAwsCredentialPersistedState(opts ...AwsCredentialPersistedStateOption) (
// ValidateCreds takes the credentials configuration from the persisted state
// and runs sts.GetCallerIdentity for the current credentials, which is done
// to check that the credentials are valid.
// to check that the credentials are valid. This method returns a status error
// with PluginError details.
func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
return errors.BadRequestStatus("missing credentials config")
}
if _, err := s.CredentialsConfig.GetCallerIdentity(ctx, s.testOpts...); err != nil {
return fmt.Errorf("error validating credentials: %w", err)
st, _ := errors.ParseAWSError(err, "validating credentials")
return st.Err()
}
return nil
}
@@ -121,18 +124,19 @@ func (s *AwsCredentialPersistedState) ValidateCreds(ctx context.Context) error {
// then deletes the old access key. If deletion of the old access key is successful, the new access key/secret key are
// written into the credentials config and the persisted state. On any error, the old credentials are not overwritten.
// This ensures that any generated new secret key never leaves this function in case of an error, even though it will
// still result in an extraneous access key existing.
// still result in an extraneous access key existing. This method returns a status error with PluginError details.
func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
return errors.BadRequestStatus("missing credentials config")
}
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
return errors.New("invalid credential type")
return errors.BadRequestStatus("invalid credential type")
}
if err := s.CredentialsConfig.RotateKeys(ctx, append([]awsutil.Option{
awsutil.WithValidityCheckTimeout(rotationWaitTimeout),
}, s.testOpts...)...); err != nil {
return fmt.Errorf("error rotating credentials: %w", err)
st, _ := errors.ParseAWSError(err, "error rotating credentials")
return st.Err()
}
s.CredsLastRotatedTime = time.Now()
return nil
@@ -140,13 +144,14 @@ func (s *AwsCredentialPersistedState) RotateCreds(ctx context.Context) error {
// 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.
// deleted first, otherwise it's left alone. This method returns a
// status error with PluginError details.
func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credentialsConfig *awsutil.CredentialsConfig) error {
if credentialsConfig == nil {
return errors.New("missing new credentials config")
return errors.BadRequestStatus("missing new credentials config")
}
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
return errors.BadRequestStatus("missing credentials config")
}
// Delete old/existing credentials. This action is only possible for static credential types.
@@ -166,27 +171,29 @@ func (s *AwsCredentialPersistedState) ReplaceCreds(ctx context.Context, credenti
// 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.
// the state just to ensure that they cannot be re-used after. This
// method returns a status error with PluginError details.
func (s *AwsCredentialPersistedState) DeleteCreds(ctx context.Context) error {
if s.CredentialsConfig == nil {
return errors.New("missing credentials config")
return errors.BadRequestStatus("missing credentials config")
}
if GetCredentialType(s.CredentialsConfig) != StaticAWS {
return errors.New("invalid credential type")
return errors.BadRequestStatus("invalid credential type")
}
if err := s.CredentialsConfig.DeleteAccessKey(ctx, s.CredentialsConfig.AccessKey, s.testOpts...); err != nil {
// Determine if the deletion error was due to a missing
// resource. If it was, just pass it.
var awsErr *iamTypes.NoSuchEntityException
if errors.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" {
if stderr.As(err, &awsErr) && awsErr.ErrorCode() == "NoSuchEntity" {
s.CredentialsConfig = nil
s.CredsLastRotatedTime = time.Time{}
return nil
}
// Otherwise treat it as an actual error.
return err
st, _ := errors.ParseAWSError(err, "failed to delete access key")
return st.Err()
}
s.CredentialsConfig = nil
+65 -39
View File
@@ -14,7 +14,10 @@ import (
"github.com/aws/aws-sdk-go-v2/service/iam"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/hashicorp/go-secure-stdlib/awsutil/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb"
)
@@ -340,14 +343,16 @@ func TestAwsCredentialPersistedStateFromProto(t *testing.T) {
func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
name string
in *AwsCredentialPersistedState
expectedStatusCode codes.Code
expectedErr string
}{
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedStatusCode: codes.InvalidArgument,
expectedErr: "missing credentials config",
},
{
name: "validation error",
@@ -365,7 +370,8 @@ func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
),
},
},
expectedErr: fmt.Sprintf("error validating credentials: %s", testGetCallerIdentityErr),
expectedStatusCode: codes.Unknown,
expectedErr: "aws service unknown: unknown error: validating credentials",
},
{
name: "good",
@@ -383,10 +389,12 @@ func TestAwsCatalogPersistedState_ValidateCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
require, assert := require.New(t), assert.New(t)
err := tc.in.ValidateCreds(context.Background())
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
assert.Equal(tc.expectedStatusCode, status.Code(err))
return
}
@@ -402,11 +410,13 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
exepected *awsutil.CredentialsConfig
expectedNonZeroRotatedTime bool
expectedErr string
expectedStatusCode codes.Code
}{
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "cannot delete dynamic credential type",
@@ -417,7 +427,8 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "cannot delete unknown credential type",
@@ -428,7 +439,8 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "rotation error",
@@ -446,7 +458,8 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
),
},
},
expectedErr: fmt.Sprintf("error rotating credentials: error calling CreateAccessKey: error calling iam.GetUser: %s", testGetUserErr),
expectedErr: "aws service unknown: unknown error: error rotating credentials",
expectedStatusCode: codes.Unknown,
},
{
name: "good",
@@ -496,10 +509,12 @@ func TestAwsCatalogPersistedState_RotateCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
require, assert := require.New(t), assert.New(t)
err := tc.in.RotateCreds(context.Background())
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
assert.Equal(tc.expectedStatusCode, status.Code(err))
return
}
@@ -521,17 +536,20 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
expected *awsutil.CredentialsConfig
expectedDeleteCalled bool
expectedErr string
expectedStatusCode codes.Code
}{
{
name: "missing new credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing new credentials config",
name: "missing new credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing new credentials config",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: "missing credentials config",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: "missing credentials config",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "deletion error",
@@ -549,8 +567,9 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
),
},
},
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
credentialConfig: &awsutil.CredentialsConfig{},
expectedErr: "aws service unknown: unknown error: failed to delete access key",
expectedStatusCode: codes.Unknown,
},
{
name: "good with delete of old rotated",
@@ -604,11 +623,12 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
require, assert := require.New(t), assert.New(t)
state.Reset()
err := tc.in.ReplaceCreds(context.Background(), tc.credentialConfig)
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
return
}
@@ -623,14 +643,16 @@ func TestAwsCatalogPersistedState_ReplaceCreds(t *testing.T) {
func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
cases := []struct {
name string
in *AwsCredentialPersistedState
expectedErr string
name string
in *AwsCredentialPersistedState
expectedErr string
expectedStatusCode codes.Code
}{
{
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
name: "missing credentials config",
in: &AwsCredentialPersistedState{},
expectedErr: "missing credentials config",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "cannot delete dynamic credential type",
@@ -641,7 +663,8 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "cannot delete unknown credential type",
@@ -652,7 +675,8 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
},
CredsLastRotatedTime: time.Now(),
},
expectedErr: "invalid credential type",
expectedErr: "invalid credential type",
expectedStatusCode: codes.InvalidArgument,
},
{
name: "deletion error",
@@ -669,7 +693,8 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
),
},
},
expectedErr: fmt.Sprintf("error deleting old access key: %s", testDeleteAccessKeyErr),
expectedErr: "aws service unknown: unknown error: failed to delete access key",
expectedStatusCode: codes.Unknown,
},
{
name: "deletion error, but OK because key was just gone",
@@ -708,10 +733,11 @@ func TestAwsCatalogPersistedState_DeleteCreds(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
require, assert := require.New(t), assert.New(t)
err := tc.in.DeleteCreds(context.Background())
if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr)
require.Error(err)
assert.ErrorContains(err, tc.expectedErr)
return
}
+76 -40
View File
@@ -18,6 +18,7 @@ import (
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/smithy-go"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/protobuf/types/known/timestamppb"
)
const (
@@ -38,6 +39,22 @@ const (
// expired. This error can only be returned by a dynamic credential
// type. This error can resolve itself by retrying the request.
awsErrorExpiredToken = "ExpiredToken"
// NoSuchBucket is returned when the specified S3 storage bucket does
// not exist
awsErrorNoSuchBucket = "NoSuchBucket"
// BadDigest is returned when an S3 PUT OBJECT request's calculated
// checksum does not match the checksum which was sent by the client
awsErrorBadDigest = "BadDigest"
// NoSuchKey is returned when attempting to interact with an S3 object
// whose key does not exist
awsErrorNoSuchKey = "NoSuchKey"
// The InvalidObjectState error is returned when trying to
// access an object that was moved into cold storage.
awsErrorInvalidObjectState = "InvalidObjectState"
)
// InvalidArgumentError returns an grpc invalid argument status error.
@@ -50,7 +67,7 @@ func InvalidArgumentError(msg string, f map[string]string) error {
sort.Strings(fieldMsgs)
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
}
return status.Error(codes.InvalidArgument, msg)
return BadRequestStatus(msg)
}
// ParseAWSError converts an aws service error into a RPC status. This method
@@ -58,9 +75,9 @@ func InvalidArgumentError(msg string, f map[string]string) error {
// and credentials. This method will fallback to parsing the http status code
// when it cannot match an aws error code. This method does not handle specific
// service type errors such as S3 or EC2.
func ParseAWSError(err error, msg string) (st *status.Status) {
func ParseAWSError(err error, msg string) (st *status.Status, permission *pb.Permission) {
if err == nil {
return nil
return nil, nil
}
// find the service name of the aws api
@@ -74,20 +91,14 @@ func ParseAWSError(err error, msg string) (st *status.Status) {
// when the aws service returns an error that
// has failed to be parsed. By default, this
// error code is retryable.
plgErr := &pb.PluginError{
Code: pb.ERROR_ERROR_UNKNOWN,
Message: err.Error(),
Nonretryable: false,
}
defer func() {
if st == nil {
statusMsg := fmt.Sprintf("aws service %s: unknown error: %s", serviceName, msg)
st = status.New(codes.Unknown, statusMsg)
}
st, err = st.WithDetails(plgErr)
if err != nil {
st = status.New(codes.Internal, err.Error())
if permission == nil {
permission = &pb.Permission{State: pb.StateType_STATE_TYPE_UNKNOWN, CheckedAt: timestamppb.Now()}
}
}()
@@ -96,9 +107,8 @@ func ParseAWSError(err error, msg string) (st *status.Status) {
Codes: retry.DefaultThrottleErrorCodes,
}
if throttleErr.IsErrorThrottle(err).Bool() {
plgErr.Code = pb.ERROR_ERROR_THROTTLING
statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg)
return status.New(codes.Unavailable, statusMsg)
return status.New(codes.Unavailable, statusMsg), nil
}
// evaluate against aws connection error codes
connectionErr := retry.IsErrorRetryables([]retry.IsErrorRetryable{
@@ -113,24 +123,40 @@ func ParseAWSError(err error, msg string) (st *status.Status) {
},
})
if connectionErr.IsErrorRetryable(err).Bool() {
plgErr.Code = pb.ERROR_ERROR_TIMEOUT
statusMsg := fmt.Sprintf("aws service %s: connectivity error: %s", serviceName, msg)
return status.New(codes.DeadlineExceeded, statusMsg)
return status.New(codes.DeadlineExceeded, statusMsg), nil
}
// evaluate against aws credentials error codes
// parse some specific aws error codes that we have special cred states for
var apiErr smithy.APIError
if errors.As(err, &apiErr) {
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
switch apiErr.ErrorCode() {
case awsErrorAccessDenied:
fallthrough
case awsErrorInvalidAccessKeyId:
plgErr.Code = pb.ERROR_ERROR_INVALID_CREDENTIAL
plgErr.Nonretryable = true
return status.New(codes.PermissionDenied, statusMsg)
fallthrough
case awsErrorExpiredToken:
plgErr.Code = pb.ERROR_ERROR_INVALID_CREDENTIAL
return status.New(codes.PermissionDenied, statusMsg)
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: apiErr.ErrorMessage(),
CheckedAt: timestamppb.Now(),
}
case awsErrorNoSuchBucket:
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: apiErr.ErrorMessage(),
CheckedAt: timestamppb.Now(),
}
case awsErrorBadDigest:
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
return status.New(codes.Aborted, statusMsg), nil
case awsErrorNoSuchKey:
fallthrough
case awsErrorInvalidObjectState:
statusMsg := fmt.Sprintf("aws %s error: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg), nil
}
}
@@ -139,34 +165,33 @@ func ParseAWSError(err error, msg string) (st *status.Status) {
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(httpErr.Response.Body)
if err != nil {
return status.New(codes.Internal, err.Error())
return status.New(codes.Internal, err.Error()), nil
}
defer httpErr.Response.Body.Close()
plgErr.Message = buf.String()
switch httpErr.HTTPStatusCode() {
case http.StatusBadRequest:
plgErr.Code = pb.ERROR_ERROR_BAD_REQUEST
plgErr.Nonretryable = true
statusMsg := fmt.Sprintf("aws service %s: bad request error: %s", serviceName, msg)
return status.New(codes.InvalidArgument, statusMsg)
return status.New(codes.InvalidArgument, statusMsg), nil
case http.StatusUnauthorized:
plgErr.Code = pb.ERROR_ERROR_INVALID_CREDENTIAL
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
return status.New(codes.PermissionDenied, statusMsg)
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: buf.String(),
CheckedAt: timestamppb.Now(),
}
case http.StatusForbidden:
plgErr.Code = pb.ERROR_ERROR_INVALID_CREDENTIAL
plgErr.Nonretryable = true
statusMsg := fmt.Sprintf("aws service %s: invalid credentials error: %s", serviceName, msg)
return status.New(codes.PermissionDenied, statusMsg)
return status.New(codes.PermissionDenied, statusMsg), &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: buf.String(),
CheckedAt: timestamppb.Now(),
}
case http.StatusNotFound:
plgErr.Code = pb.ERROR_ERROR_BAD_REQUEST
plgErr.Nonretryable = true
statusMsg := fmt.Sprintf("aws service %s: resource not found error: %s", serviceName, msg)
return status.New(codes.NotFound, statusMsg)
return status.New(codes.NotFound, statusMsg), nil
case http.StatusTooManyRequests:
plgErr.Code = pb.ERROR_ERROR_THROTTLING
statusMsg := fmt.Sprintf("aws service %s: throttling error: %s", serviceName, msg)
return status.New(codes.Unavailable, statusMsg)
return status.New(codes.Unavailable, statusMsg), nil
case http.StatusRequestTimeout:
fallthrough
case http.StatusInternalServerError:
@@ -176,11 +201,22 @@ func ParseAWSError(err error, msg string) (st *status.Status) {
case http.StatusServiceUnavailable:
fallthrough
case http.StatusGatewayTimeout:
plgErr.Code = pb.ERROR_ERROR_TIMEOUT
statusMsg := fmt.Sprintf("aws service %s: connectivity error: %s", serviceName, msg)
return status.New(codes.DeadlineExceeded, statusMsg)
return status.New(codes.DeadlineExceeded, statusMsg), nil
}
}
return st
return st, nil
}
// BadRequestStatus returns a status error with an invalid
// argument code
func BadRequestStatus(format string, args ...any) error {
return status.New(codes.InvalidArgument, fmt.Sprintf(format, args...)).Err()
}
// UnknownStatus returns a status error with an internal
// error code and a retryable plugin error detail.
func UnknownStatus(format string, args ...any) error {
return status.New(codes.Internal, fmt.Sprintf(format, args...)).Err()
}
@@ -7,19 +7,21 @@ import (
"net/http"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/protobuf/types/known/timestamppb"
)
func Test_ParseAWSError(t *testing.T) {
t.Parallel()
cases := []struct {
name string
err error
expectedStatusCode codes.Code
expectedStatusMsg string
expectedPluginError *pb.PluginError
name string
err error
expectedStatusCode codes.Code
expectedStatusMsg string
expectedPermission *pb.Permission
}{
{
name: "nil-error",
@@ -30,10 +32,8 @@ func Test_ParseAWSError(t *testing.T) {
err: fmt.Errorf("what can this error be?"),
expectedStatusCode: codes.Unknown,
expectedStatusMsg: "aws service unknown: unknown error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_UNKNOWN,
Message: "what can this error be?",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -41,10 +41,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusBadRequest, "bad request"),
expectedStatusCode: codes.InvalidArgument,
expectedStatusMsg: "aws service unknown: bad request error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_BAD_REQUEST,
Message: "bad request",
Nonretryable: true,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -52,10 +50,10 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusUnauthorized, "unauthorized request"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_INVALID_CREDENTIAL,
Message: "unauthorized request",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "unauthorized request",
CheckedAt: timestamppb.Now(),
},
},
{
@@ -63,10 +61,10 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusForbidden, "forbidden request"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_INVALID_CREDENTIAL,
Message: "forbidden request",
Nonretryable: true,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "forbidden request",
CheckedAt: timestamppb.Now(),
},
},
{
@@ -74,10 +72,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusNotFound, "not found request"),
expectedStatusCode: codes.NotFound,
expectedStatusMsg: "aws service unknown: resource not found error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_BAD_REQUEST,
Message: "not found request",
Nonretryable: true,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -85,10 +81,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusTooManyRequests, "too many requests"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "too many requests",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -96,10 +90,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusRequestTimeout, "request timeout"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: connectivity error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_TIMEOUT,
Message: "request timeout",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -107,10 +99,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusInternalServerError, "internal server error"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: connectivity error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_TIMEOUT,
Message: "https response error StatusCode: 500",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -118,10 +108,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusBadGateway, "bad gateway"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: connectivity error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_TIMEOUT,
Message: "https response error StatusCode: 502",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -129,10 +117,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusServiceUnavailable, "service unavailable"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: connectivity error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_TIMEOUT,
Message: "https response error StatusCode: 503",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -140,10 +126,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsHttpResponseError(http.StatusGatewayTimeout, "gateway timeout"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: connectivity error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_TIMEOUT,
Message: "https response error StatusCode: 504",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -151,10 +135,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("Throttling", "throttling error"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "throttling error",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -162,10 +144,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("ThrottlingException", "throttling exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "throttling exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -173,10 +153,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("ThrottledException", "throttled exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "throttled exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -184,10 +162,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("RequestThrottledException", "request throttled exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "request throttled exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -195,10 +171,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("TooManyRequestsException", "too many requests exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "too many requests exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -206,10 +180,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("ProvisionedThroughputExceededException", "provisioned throughput exceeded exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "provisioned throughput exceeded exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -217,10 +189,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("TransactionInProgressException", "transaction in progress exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "transaction in progress exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -228,10 +198,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("RequestLimitExceeded", "request limit exceeded"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "request limit exceeded",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -239,10 +207,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("BandwidthLimitExceeded", "bandwidth limit exceeded"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "bandwidth limit exceeded",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -250,10 +216,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("LimitExceededException", "limit exceeded exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "limit exceeded exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -261,10 +225,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("RequestThrottled", "request throttled"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "request throttled",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -272,10 +234,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("SlowDown", "slow down"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "slow down",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -283,10 +243,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("PriorRequestNotComplete", "prior request not complete"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "prior request not complete",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -294,10 +252,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("EC2ThrottledException", "ec2 throttled exception"),
expectedStatusCode: codes.Unavailable,
expectedStatusMsg: "aws service unknown: throttling error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_THROTTLING,
Message: "ec2 throttled exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -305,10 +261,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("RequestTimeout", "request timeout"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: connectivity error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_TIMEOUT,
Message: "request timeout",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -316,10 +270,8 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError("RequestTimeoutException", "request timeout exception"),
expectedStatusCode: codes.DeadlineExceeded,
expectedStatusMsg: "aws service unknown: connectivity error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_TIMEOUT,
Message: "request timeout exception",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_UNKNOWN,
},
},
{
@@ -327,10 +279,10 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError(awsErrorAccessDenied, "not authorized to perform action"),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_INVALID_CREDENTIAL,
Message: "not authorized to perform action",
Nonretryable: true,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "not authorized to perform action",
CheckedAt: timestamppb.Now(),
},
},
{
@@ -338,10 +290,10 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError(awsErrorInvalidAccessKeyId, "The AWS Access Key Id you provided does not exist in our records."),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_INVALID_CREDENTIAL,
Message: "The AWS Access Key Id you provided does not exist in our records.",
Nonretryable: true,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "The AWS Access Key Id you provided does not exist in our records.",
CheckedAt: timestamppb.Now(),
},
},
{
@@ -349,18 +301,18 @@ func Test_ParseAWSError(t *testing.T) {
err: TestAwsError(awsErrorExpiredToken, "The security token included in the request is expired."),
expectedStatusCode: codes.PermissionDenied,
expectedStatusMsg: "aws service unknown: invalid credentials error: test",
expectedPluginError: &pb.PluginError{
Code: pb.ERROR_ERROR_INVALID_CREDENTIAL,
Message: "The security token included in the request is expired.",
Nonretryable: false,
expectedPermission: &pb.Permission{
State: pb.StateType_STATE_TYPE_ERROR,
ErrorDetails: "The security token included in the request is expired.",
CheckedAt: timestamppb.Now(),
},
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
require, assert := require.New(t), assert.New(t)
actualStatus := ParseAWSError(tt.err, "test")
actualStatus, permission := ParseAWSError(tt.err, "test")
if tt.expectedStatusCode == codes.OK {
require.Nil(actualStatus)
return
@@ -368,15 +320,48 @@ func Test_ParseAWSError(t *testing.T) {
require.Equal(tt.expectedStatusCode, actualStatus.Code())
require.Equal(tt.expectedStatusMsg, actualStatus.Message())
require.Len(actualStatus.Details(), 1)
for _, detail := range actualStatus.Details() {
switch errDetail := detail.(type) {
case *pb.PluginError:
require.Equal(tt.expectedPluginError.Code, errDetail.Code)
require.Contains(errDetail.Message, tt.expectedPluginError.Message)
require.Equal(tt.expectedPluginError.Nonretryable, errDetail.Nonretryable)
}
}
require.Len(actualStatus.Details(), 0)
CheckSimilarPermission(assert, tt.expectedPermission, permission, true)
})
}
}
// func Test_BadRequestStatus(t *testing.T) {
// require, assert := require.New(t), assert.New(t)
// err := BadRequestStatus("test: %s", "hello world")
// assert.ErrorContains(err, "test: hello world")
// assert.Equal(status.Code(err), codes.InvalidArgument)
// st, ok := status.FromError(err)
// require.True(ok)
// var plgErr *pb.PluginError
// for _, detail := range st.Details() {
// if errDetail, ok := detail.(*pb.PluginError); ok {
// plgErr = errDetail
// break
// }
// }
// require.NotNil(plgErr)
// assert.Equal(pb.ERROR_ERROR_BAD_REQUEST, plgErr.Code)
// assert.Equal("bad request", plgErr.Message)
// assert.True(plgErr.Nonretryable)
// }
// func Test_UnknownStatus(t *testing.T) {
// require, assert := require.New(t), assert.New(t)
// err := UnknownStatus("test: %s", "hello world")
// assert.ErrorContains(err, "test: hello world")
// assert.Equal(status.Code(err), codes.Internal)
// st, ok := status.FromError(err)
// require.True(ok)
// var plgErr *pb.PluginError
// for _, detail := range st.Details() {
// if errDetail, ok := detail.(*pb.PluginError); ok {
// plgErr = errDetail
// break
// }
// }
// require.NotNil(plgErr)
// assert.Equal(pb.ERROR_ERROR_UNKNOWN, plgErr.Code)
// assert.Equal("unknown internal error", plgErr.Message)
// assert.False(plgErr.Nonretryable)
// }
+23
View File
@@ -8,6 +8,9 @@ import (
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/smithy-go"
smithyhttp "github.com/aws/smithy-go/transport/http"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/types/known/timestamppb"
)
// TestAwsHttpResponseError returns an aws http response error
@@ -32,3 +35,23 @@ func TestAwsError(code, msg string) error {
Fault: smithy.FaultServer,
}
}
func CheckSimilarPermission(assert *assert.Assertions, expected, actual *pb.Permission, substituteNilWithDefaultOk bool) {
if expected == nil && substituteNilWithDefaultOk {
expected = &pb.Permission{
State: pb.StateType_STATE_TYPE_OK,
CheckedAt: timestamppb.Now(),
}
}
if expected != nil {
assert.NotNil(actual)
if actual != nil {
assert.Equal(expected.State, actual.State, "StateType mismatch")
assert.Equal(expected.ErrorDetails, actual.ErrorDetails)
assert.NotNil(actual.CheckedAt)
}
} else {
assert.Equal(expected, actual)
}
}