Files
teleport/lib/cloud/aws/errors_test.go
T
Marco Dinis 4b2a0da91d Fix AWS ListDeployedDatabaseServices when there's no ECS Cluster (#50843)
Calling the AWS API `ecs:ListServices` with a non-existent ECS Cluster
name will return a 400 w/ ClusterNotFoundException.

The existing code was not handling that error and a raw error was
returned.

This PR changes the logic to ensure that case is handled and that the
ListDeployedDatabaseServices returns an empty list.

An alternative would be to call the ListClusters beforehand, but that
would increase the number of API calls we do to external services.
2025-01-09 09:45:41 +00:00

201 lines
5.9 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package aws
import (
"errors"
"net/http"
"testing"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
smithyhttp "github.com/aws/smithy-go/transport/http"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
)
func TestConvertRequestFailureError(t *testing.T) {
t.Parallel()
fakeRequestID := "11111111-2222-3333-3333-333333333334"
tests := []struct {
name string
inputError error
wantUnmodified bool
wantIsError func(error) bool
}{
{
name: "StatusForbidden",
inputError: awserr.NewRequestFailure(awserr.New("code", "message", nil), http.StatusForbidden, fakeRequestID),
wantIsError: trace.IsAccessDenied,
},
{
name: "StatusConflict",
inputError: awserr.NewRequestFailure(awserr.New("code", "message", nil), http.StatusConflict, fakeRequestID),
wantIsError: trace.IsAlreadyExists,
},
{
name: "StatusNotFound",
inputError: awserr.NewRequestFailure(awserr.New("code", "message", nil), http.StatusNotFound, fakeRequestID),
wantIsError: trace.IsNotFound,
},
{
name: "StatusBadRequest",
inputError: awserr.NewRequestFailure(awserr.New("code", "message", nil), http.StatusBadRequest, fakeRequestID),
wantUnmodified: true,
},
{
name: "StatusBadRequest with AccessDeniedException",
inputError: awserr.NewRequestFailure(awserr.New("AccessDeniedException", "message", nil), http.StatusBadRequest, fakeRequestID),
wantIsError: trace.IsAccessDenied,
},
{
name: "not AWS error",
inputError: errors.New("not-aws-error"),
wantUnmodified: true,
},
{
name: "v2 sdk error",
inputError: &awshttp.ResponseError{
ResponseError: &smithyhttp.ResponseError{
Response: &smithyhttp.Response{Response: &http.Response{
StatusCode: http.StatusNotFound,
}},
Err: trace.Errorf(""),
},
},
wantIsError: trace.IsNotFound,
},
{
name: "v2 sdk error for ecs ClusterNotFoundException",
inputError: &awshttp.ResponseError{
ResponseError: &smithyhttp.ResponseError{
Response: &smithyhttp.Response{Response: &http.Response{
StatusCode: http.StatusBadRequest,
}},
Err: trace.Errorf("ClusterNotFoundException"),
},
},
wantIsError: trace.IsNotFound,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := ConvertRequestFailureError(test.inputError)
if test.wantUnmodified {
require.Equal(t, test.inputError, err)
} else {
require.True(t, test.wantIsError(err))
}
})
}
}
func TestConvertIAMv2Error(t *testing.T) {
for _, tt := range []struct {
name string
inErr error
errCheck require.ErrorAssertionFunc
}{
{
name: "no error",
inErr: nil,
errCheck: require.NoError,
},
{
name: "resource already exists",
inErr: &iamtypes.EntityAlreadyExistsException{
Message: aws.String("resource exists"),
},
errCheck: func(tt require.TestingT, err error, i ...interface{}) {
require.True(tt, trace.IsAlreadyExists(err), "expected trace.AlreadyExists error, got %v", err)
},
},
{
name: "resource already exists",
inErr: &iamtypes.NoSuchEntityException{
Message: aws.String("resource not found"),
},
errCheck: func(tt require.TestingT, err error, i ...interface{}) {
require.True(tt, trace.IsNotFound(err), "expected trace.NotFound error, got %v", err)
},
},
{
name: "invalid policy document",
inErr: &iamtypes.MalformedPolicyDocumentException{
Message: aws.String("malformed document"),
},
errCheck: func(tt require.TestingT, err error, i ...interface{}) {
require.True(tt, trace.IsBadParameter(err), "expected trace.BadParameter error, got %v", err)
},
},
{
name: "unauthorized",
inErr: &awshttp.ResponseError{
ResponseError: &smithyhttp.ResponseError{
Response: &smithyhttp.Response{Response: &http.Response{
StatusCode: http.StatusForbidden,
}},
Err: trace.Errorf(""),
},
},
errCheck: func(tt require.TestingT, err error, i ...interface{}) {
require.True(tt, trace.IsAccessDenied(err), "expected trace.AccessDenied error, got %v", err)
},
},
{
name: "not found",
inErr: &awshttp.ResponseError{
ResponseError: &smithyhttp.ResponseError{
Response: &smithyhttp.Response{Response: &http.Response{
StatusCode: http.StatusNotFound,
}},
Err: trace.Errorf(""),
},
},
errCheck: func(tt require.TestingT, err error, i ...interface{}) {
require.True(tt, trace.IsNotFound(err), "expected trace.NotFound error, got %v", err)
},
},
{
name: "resource already exists",
inErr: &awshttp.ResponseError{
ResponseError: &smithyhttp.ResponseError{
Response: &smithyhttp.Response{Response: &http.Response{
StatusCode: http.StatusConflict,
}},
Err: trace.Errorf(""),
},
},
errCheck: func(tt require.TestingT, err error, i ...interface{}) {
require.True(tt, trace.IsAlreadyExists(err), "expected trace.AlreadyExists error, got %v", err)
},
},
} {
t.Run(tt.name, func(t *testing.T) {
tt.errCheck(t, ConvertIAMv2Error(tt.inErr))
})
}
}