Files
threshold-plugin-aws/internal/errors/error.go
T

187 lines
6.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package errors
import (
"bytes"
"errors"
"fmt"
"net/http"
"sort"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/aws/aws-sdk-go-v2/aws/retry"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/smithy-go"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
)
const (
// awsErrorAccessDenied is returned when the credentials do not have
// the required permissions to do a specific action on a resource.
// This error will persist until the IAM Role Policy is updated by
// the owner. This error cannot be resolved by retrying the request.
awsErrorAccessDenied = "AccessDenied"
// awsErrorInvalidAccessKeyId is returned when the credential does
// not exist. This error will persist until the credentials attached
// to the storage bucket is updated. This error can occur when the
// given credentials are deleted, deactivated, or rotated. This
// error cannot be resolved by retrying the request.
awsErrorInvalidAccessKeyId = "InvalidAccessKeyId"
// awsErrorExpiredToken is returned when the provided credential is
// expired. This error can only be returned by a dynamic credential
// type. This error can resolve itself by retrying the request.
awsErrorExpiredToken = "ExpiredToken"
)
// InvalidArgumentError returns an grpc invalid argument status error.
func InvalidArgumentError(msg string, f map[string]string) error {
var fieldMsgs []string
for field, val := range f {
fieldMsgs = append(fieldMsgs, fmt.Sprintf("%s: %s", field, val))
}
if len(fieldMsgs) > 0 {
sort.Strings(fieldMsgs)
msg = fmt.Sprintf("%s: [%s]", msg, strings.Join(fieldMsgs, ", "))
}
return status.Error(codes.InvalidArgument, msg)
}
// ParseAWSError converts an aws service error into a RPC status. This method
// will handle parsing the following aws error codes: throttling, connections,
// 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) {
if err == nil {
return nil
}
// find the service name of the aws api
serviceName := "unknown"
var oe *smithy.OperationError
if errors.As(err, &oe) {
serviceName = oe.Service()
}
// The unknown plugin error code will be used
// 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())
}
}()
// evaluate against aws throttling error codes
throttleErr := retry.ThrottleErrorCode{
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)
}
// evaluate against aws connection error codes
connectionErr := retry.IsErrorRetryables([]retry.IsErrorRetryable{
retry.NoRetryCanceledError{},
retry.RetryableError{},
retry.RetryableConnectionError{},
retry.RetryableHTTPStatusCode{
Codes: retry.DefaultRetryableHTTPStatusCodes,
},
retry.RetryableErrorCode{
Codes: retry.DefaultRetryableErrorCodes,
},
})
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)
}
// evaluate against aws credentials error codes
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)
case awsErrorExpiredToken:
plgErr.Code = pb.ERROR_ERROR_INVALID_CREDENTIAL
return status.New(codes.PermissionDenied, statusMsg)
}
}
// default to evaluating the http status code
if httpErr, ok := err.(*awshttp.ResponseError); ok {
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(httpErr.Response.Body)
if err != nil {
return status.New(codes.Internal, err.Error())
}
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)
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)
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)
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)
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)
case http.StatusRequestTimeout:
fallthrough
case http.StatusInternalServerError:
fallthrough
case http.StatusBadGateway:
fallthrough
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 st
}