Compare commits

...
Author SHA1 Message Date
A-440Hz 95f9e80b81 test: refactor error syntax 2022-03-10 10:03:37 -08:00
A-440Hz 9e1fb04057 test: update tests to validate error codes 2022-03-09 15:23:22 -08:00
A-440Hz 4656760fb8 refactor: update errors to include error codes 2022-03-08 16:12:16 -08:00
Chris Marchesi 6ce3e27677 Merge pull request #8 from hashicorp/update-readme-cli-example
Update README to have full, correct examples
2022-01-04 16:47:53 -08:00
Chris Marchesi d348501d34 Add examples for providing multiple filters 2022-01-04 16:04:22 -08:00
Chris Marchesi 9f25f80ba9 Update README to have full, correct examples
This updates the README to have full examples for both host set and host
catalog creation, and also corrects an incorrect example in the JSON
example (tag-name was supposed to be tag-key).

Additionally, I've removed the JSON example as it's no longer needed as
the CLI corrects accepts filter names with dashes with the work
performed in #2 and #4.
2022-01-04 15:06:59 -08:00
Chris Marchesi e565bad027 Merge pull request #7 from hashicorp/vancluever-add-dns-names
plugin: add DNS names
2021-12-06 15:21:53 -08:00
5 changed files with 325 additions and 174 deletions

No files matched your search

+56 -10
View File
@@ -26,6 +26,62 @@ given credentials will be used to create a new credential, and then the given
credential will be revoked. In this way, after rotation, only Boundary knows the credential will be revoked. In this way, after rotation, only Boundary knows the
client secret in use by this plugin. client secret in use by this plugin.
## Getting Started
Refer to [Attributes and Secrets](#attributes-and-secrets) for more detail on
configuration options for host catalogs and sets.
To create a host catalog (using default scope created by `boundary dev`):
```
boundary host-catalogs create plugin \
-scope-id p_1234567890 \
-name "Example Plugin-Based Host Catalog" \
-description "Description for plugin-based host catalog" \
-plugin-name aws \
-attr region=REGION \
-secret access_key_id='KEY' \
-secret secret_access_key='SECRET'
```
To create a host set, filtering the host set based on tag keys `foo` or `bar`
(either tag can be present), ensuring that any targets set to this host set only
connect to external addresses in the `54.0.0.0/8` class A subnet:
```
boundary host-sets create plugin \
-host-catalog-id HOST_CATALOG_ID \
-name "Example Plugin-Based Host Set" \
-description "Description for plugin-based host set" \
-attr filters=tag-key=foo,bar \
-preferred-endpoint "cidr:54.0.0.0/8"
```
As above, but instances must have both tags (both `foo` and `bar` *must* be
present):
```
boundary host-sets create plugin \
-host-catalog-id HOST_CATALOG_ID \
-name "Example Plugin-Based Host Set" \
-description "Description for plugin-based host set" \
-attr filters=tag-key=foo \
-attr filters=tag-key=bar \
-preferred-endpoint "cidr:54.0.0.0/8"
```
As above, but matching on tag key and launch date:
```
boundary host-sets create plugin \
-host-catalog-id HOST_CATALOG_ID \
-name "Example Plugin-Based Host Set" \
-description "Description for plugin-based host set" \
-attr filters=tag-key=foo \
-attr filters=launch-time=2022-01-04T* \
-preferred-endpoint "cidr:54.0.0.0/8"
```
## Required IAM Privileges ## Required IAM Privileges
The following IAM privileges, at the very least, are required to be attached to The following IAM privileges, at the very least, are required to be attached to
@@ -97,13 +153,3 @@ The following attributes are valid on an AWS host Set resource:
a comma-separated list. For a list of filter options, check out a comma-separated list. For a list of filter options, check out
[describe-instances in the AWS CLI [describe-instances in the AWS CLI
reference](https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html). reference](https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html).
#### Supplying Filters on the Boundary CLI
As `filters` is part of a host set's attributes and may contain dashes which are
not identifier-friendly, it's recommended you that supply attributes for AWS
host sets as a full JSON string. Example:
```
boundary host-sets create plugin -host-catalog-id hc_1234567890 --attributes '{"filters":{"tag-name":["foo"]}}'
```
+39 -11
View File
@@ -7,9 +7,23 @@ import (
"time" "time"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
) )
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)
}
// CatalogAttributes is a Go-native representation of the Attributes // CatalogAttributes is a Go-native representation of the Attributes
// map. // map.
type CatalogAttributes struct { type CatalogAttributes struct {
@@ -20,22 +34,27 @@ type CatalogAttributes struct {
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) { func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
unknownFields := structFields(in) unknownFields := structFields(in)
result := new(CatalogAttributes) result := new(CatalogAttributes)
badFields := make(map[string]string)
var err error var err error
result.Region, err = getStringValue(in, constRegion, true) result.Region, err = getStringValue(in, constRegion, true)
if err != nil { if err != nil {
return nil, err badFields[fmt.Sprintf("attributes.%s", constRegion)] = err.Error()
} }
delete(unknownFields, constRegion) delete(unknownFields, constRegion)
result.DisableCredentialRotation, err = getBoolValue(in, constDisableCredentialRotation, false) result.DisableCredentialRotation, err = getBoolValue(in, constDisableCredentialRotation, false)
if err != nil { if err != nil {
return nil, err badFields[fmt.Sprintf("attributes.%s", constDisableCredentialRotation)] = err.Error()
} }
delete(unknownFields, constDisableCredentialRotation) delete(unknownFields, constDisableCredentialRotation)
if len(unknownFields) != 0 { for s := range unknownFields {
return nil, fmt.Errorf("unknown catalog attribute fields provided: %s", keysAsString(unknownFields)) badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
}
if len(badFields) > 0 {
return nil, invalidArgumentError("Invalid arguments in catalog attributes", badFields)
} }
return result, nil return result, nil
@@ -50,22 +69,27 @@ type CatalogSecrets struct {
func getCatalogSecrets(in *structpb.Struct) (*CatalogSecrets, error) { func getCatalogSecrets(in *structpb.Struct) (*CatalogSecrets, error) {
unknownFields := structFields(in) unknownFields := structFields(in)
result := new(CatalogSecrets) result := new(CatalogSecrets)
badFields := make(map[string]string)
var err error var err error
result.AccessKeyId, err = getStringValue(in, constAccessKeyId, true) result.AccessKeyId, err = getStringValue(in, constAccessKeyId, true)
if err != nil { if err != nil {
return nil, err badFields[fmt.Sprintf("secrets.%s", constAccessKeyId)] = err.Error()
} }
delete(unknownFields, constAccessKeyId) delete(unknownFields, constAccessKeyId)
result.SecretAccessKey, err = getStringValue(in, constSecretAccessKey, true) result.SecretAccessKey, err = getStringValue(in, constSecretAccessKey, true)
if err != nil { if err != nil {
return nil, err badFields[fmt.Sprintf("secrets.%s", constSecretAccessKey)] = err.Error()
} }
delete(unknownFields, constSecretAccessKey) delete(unknownFields, constSecretAccessKey)
if len(unknownFields) != 0 { for s := range unknownFields {
return nil, fmt.Errorf("unknown catalog secret fields provided: %s", keysAsString(unknownFields)) badFields[fmt.Sprintf("secrets.%s", s)] = "unrecognized field"
}
if len(badFields) > 0 {
return nil, invalidArgumentError("Error in the secrets provided", badFields)
} }
return result, nil return result, nil
@@ -84,9 +108,13 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
// here for now. Make this more complex if we add more attributes // here for now. Make this more complex if we add more attributes
// to host sets. // to host sets.
unknownFields := structFields(in) unknownFields := structFields(in)
badFields := make(map[string]string)
delete(unknownFields, constDescribeInstancesFilters) delete(unknownFields, constDescribeInstancesFilters)
if len(unknownFields) != 0 { for a := range unknownFields {
return nil, fmt.Errorf("unknown set attribute fields provided: %s", keysAsString(unknownFields)) badFields[fmt.Sprintf("attributes.%s", a)] = "unrecognized field"
}
if len(badFields) > 0 {
return nil, invalidArgumentError("Error in the attributes provided", badFields)
} }
// Mapstructure complains if it expects a slice as output and sees a scalar // Mapstructure complains if it expects a slice as output and sees a scalar
@@ -101,7 +129,7 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
} }
if err := mapstructure.Decode(inMap, &setAttrs); err != nil { if err := mapstructure.Decode(inMap, &setAttrs); err != nil {
return nil, fmt.Errorf("error decoding set attributes: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
} }
return &setAttrs, nil return &setAttrs, nil
+10 -4
View File
@@ -5,6 +5,8 @@ import (
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
) )
@@ -35,7 +37,7 @@ func TestGetCatalogAttributes(t *testing.T) {
"foo": true, "foo": true,
"bar": true, "bar": true,
}), }),
expectedErrContains: "unknown catalog attribute fields provided: bar, foo", expectedErrContains: "attributes.bar: Unrecognized field, attributes.foo: Unrecognized field",
}, },
{ {
name: "default", name: "default",
@@ -68,6 +70,7 @@ func TestGetCatalogAttributes(t *testing.T) {
if tc.expectedErrContains != "" { if tc.expectedErrContains != "" {
require.Error(err) require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err), codes.InvalidArgument)
return return
} }
@@ -104,7 +107,7 @@ func TestGetCatalogSecrets(t *testing.T) {
"foo": true, "foo": true,
"bar": true, "bar": true,
}), }),
expectedErrContains: "unknown catalog secret fields provided: bar, foo", expectedErrContains: "secrets.bar: Unrecognized field, secrets.foo: Unrecognized field",
}, },
{ {
name: "good", name: "good",
@@ -127,6 +130,7 @@ func TestGetCatalogSecrets(t *testing.T) {
if tc.expectedErrContains != "" { if tc.expectedErrContains != "" {
require.Error(err) require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err), codes.InvalidArgument)
return return
} }
@@ -170,7 +174,7 @@ func TestGetSetAttributes(t *testing.T) {
"foo": true, "foo": true,
"bar": true, "bar": true,
}), }),
expectedErrContains: "unknown set attribute fields provided: bar, foo", expectedErrContains: "attributes.bar: Unrecognized field, attributes.foo: Unrecognized field",
}, },
{ {
name: "good", name: "good",
@@ -194,6 +198,7 @@ func TestGetSetAttributes(t *testing.T) {
if tc.expectedErrContains != "" { if tc.expectedErrContains != "" {
require.Error(err) require.Error(err)
require.Contains(err.Error(), tc.expectedErrContains) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err), codes.InvalidArgument)
return return
} }
@@ -413,7 +418,8 @@ func TestValidateRegion(t *testing.T) {
require := require.New(t) require := require.New(t)
err := validateRegion(tc.in) err := validateRegion(tc.in)
if tc.expectedErr != "" { if tc.expectedErr != "" {
require.EqualError(err, tc.expectedErr) require.Contains(err.Error(), tc.expectedErr)
require.Equal(status.Code(err), codes.InvalidArgument)
return return
} }
+56 -54
View File
@@ -12,6 +12,8 @@ import (
"github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/endpoints"
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin" pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
// rotationWaitTimeout controls the time we wait for credential rotation to // rotationWaitTimeout controls the time we wait for credential rotation to
@@ -36,17 +38,17 @@ var (
func (p *AwsPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) { func (p *AwsPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
catalog := req.GetCatalog() catalog := req.GetCatalog()
if catalog == nil { if catalog == nil {
return nil, errors.New("catalog is nil") return nil, status.Error(codes.InvalidArgument, "catalog is nil")
} }
secrets := catalog.GetSecrets() secrets := catalog.GetSecrets()
if secrets == nil { if secrets == nil {
return nil, errors.New("secrets are required") return nil, status.Error(codes.InvalidArgument, "secrets are required")
} }
attrs := catalog.GetAttributes() attrs := catalog.GetAttributes()
if attrs == nil { if attrs == nil {
return nil, errors.New("attributes are required") return nil, status.Error(codes.InvalidArgument, "attributes are required")
} }
catalogAttributes, err := getCatalogAttributes(attrs) catalogAttributes, err := getCatalogAttributes(attrs)
@@ -68,24 +70,24 @@ func (p *AwsPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalog
withSecretAccessKey(catalogSecrets.SecretAccessKey), withSecretAccessKey(catalogSecrets.SecretAccessKey),
}, p.testStateOpts...)...) }, p.testStateOpts...)...)
if err != nil { if err != nil {
return nil, fmt.Errorf("error setting up persisted state: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error setting up persisted state: %s", err)
} }
// Try to rotate the credentials if we're not skipping them. // Try to rotate the credentials if we're not skipping them.
if !catalogAttributes.DisableCredentialRotation { if !catalogAttributes.DisableCredentialRotation {
if err := state.RotateCreds(); err != nil { if err := state.RotateCreds(); err != nil {
return nil, fmt.Errorf("error during credential rotation: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
} }
} else { } else {
// Simply validate if we aren't rotating. // Simply validate if we aren't rotating.
if err := state.ValidateCreds(); err != nil { if err := state.ValidateCreds(); err != nil {
return nil, fmt.Errorf("error during credential validation: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error during credential validation: %s", err)
} }
} }
persistedProto, err := state.ToProto() persistedProto, err := state.ToProto()
if err != nil { if err != nil {
return nil, err return nil, status.Error(codes.InvalidArgument, err.Error())
} }
return &pb.OnCreateCatalogResponse{ return &pb.OnCreateCatalogResponse{
@@ -99,7 +101,7 @@ func (p *AwsPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalog
// fetching the new copy to keep things a little less messy. // fetching the new copy to keep things a little less messy.
catalog := req.GetNewCatalog() catalog := req.GetNewCatalog()
if catalog == nil { if catalog == nil {
return nil, errors.New("new catalog is nil") return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
} }
var updateSecrets bool var updateSecrets bool
@@ -112,7 +114,7 @@ func (p *AwsPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalog
attrs := catalog.GetAttributes() attrs := catalog.GetAttributes()
if attrs == nil { if attrs == nil {
return nil, errors.New("new catalog missing attributes") return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
} }
catalogAttributes, err := getCatalogAttributes(attrs) catalogAttributes, err := getCatalogAttributes(attrs)
@@ -134,7 +136,7 @@ func (p *AwsPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalog
// is fine and important, but this may change in the future. // is fine and important, but this may change in the future.
state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...) state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("error loading persisted state: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
} }
if catalogAttributes.DisableCredentialRotation && !updateSecrets { if catalogAttributes.DisableCredentialRotation && !updateSecrets {
@@ -142,7 +144,7 @@ func (p *AwsPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalog
// rotation for credentials currently being managed by rotation. // rotation for credentials currently being managed by rotation.
// This is not allowed. // This is not allowed.
if !state.CredsLastRotatedTime.IsZero() { if !state.CredsLastRotatedTime.IsZero() {
return nil, errors.New("cannot disable rotation for already-rotated credentials") return nil, status.Error(codes.FailedPrecondition, "cannot disable rotation for already-rotated credentials")
} }
} }
@@ -156,7 +158,7 @@ func (p *AwsPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalog
// rotation time as well and deletes the credentials if we are // rotation time as well and deletes the credentials if we are
// managing them (ie: if we've rotated them before). // managing them (ie: if we've rotated them before).
if err := state.ReplaceCreds(catalogSecrets.AccessKeyId, catalogSecrets.SecretAccessKey); err != nil { if err := state.ReplaceCreds(catalogSecrets.AccessKeyId, catalogSecrets.SecretAccessKey); err != nil {
return nil, fmt.Errorf("error attempting to replace credentials: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error attempting to replace credentials: %s", err)
} }
} }
@@ -164,14 +166,14 @@ func (p *AwsPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalog
// If we're enabling rotation now but didn't before, or have // If we're enabling rotation now but didn't before, or have
// freshly replaced credentials, we can rotate here. // freshly replaced credentials, we can rotate here.
if err := state.RotateCreds(); err != nil { if err := state.RotateCreds(); err != nil {
return nil, fmt.Errorf("error during credential rotation: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error during credential rotation: %s", err)
} }
} }
// That's it! // That's it!
persistedProto, err := state.ToProto() persistedProto, err := state.ToProto()
if err != nil { if err != nil {
return nil, err return nil, status.Error(codes.InvalidArgument, err.Error())
} }
return &pb.OnUpdateCatalogResponse{ return &pb.OnUpdateCatalogResponse{
@@ -188,7 +190,7 @@ func (p *AwsPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalog
// may be corrupt/and or legitimately missing. // may be corrupt/and or legitimately missing.
state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...) state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("error loading persisted state: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
} }
if !state.CredsLastRotatedTime.IsZero() { if !state.CredsLastRotatedTime.IsZero() {
@@ -196,7 +198,7 @@ func (p *AwsPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalog
// credentials to ensure that it has the proper permissions to do // credentials to ensure that it has the proper permissions to do
// it. // it.
if err := state.DeleteCreds(); err != nil { if err := state.DeleteCreds(); err != nil {
return nil, fmt.Errorf("error removing rotated credentials during catalog deletion: %w", err) return nil, status.Errorf(codes.Aborted, "error removing rotated credentials during catalog deletion: %s", err)
} }
} }
@@ -206,54 +208,54 @@ func (p *AwsPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalog
func (p *AwsPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) { func (p *AwsPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
catalog := req.GetCatalog() catalog := req.GetCatalog()
if catalog == nil { if catalog == nil {
return nil, errors.New("catalog is nil") return nil, status.Error(codes.InvalidArgument, "catalog is nil")
} }
catalogAttrsRaw := catalog.GetAttributes() catalogAttrsRaw := catalog.GetAttributes()
if catalogAttrsRaw == nil { if catalogAttrsRaw == nil {
return nil, errors.New("catalog missing attributes") return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
} }
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw) catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
if err != nil { if err != nil {
return nil, fmt.Errorf("integrity error in host catalog attributes: %w", err) return nil, err
} }
if err := validateRegion(catalogAttributes.Region); err != nil { if err := validateRegion(catalogAttributes.Region); err != nil {
return nil, fmt.Errorf("integrity error in host catalog attributes: %w", err) return nil, err
} }
state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...) state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("error loading persisted state: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
} }
set := req.GetSet() set := req.GetSet()
if set == nil { if set == nil {
return nil, errors.New("set is nil") return nil, status.Error(codes.InvalidArgument, "set is nil")
} }
if set.GetAttributes() == nil { if set.GetAttributes() == nil {
return nil, errors.New("set missing attributes") return nil, status.Error(codes.InvalidArgument, "set missing attributes")
} }
setAttrs, err := getSetAttributes(set.GetAttributes()) setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing set attributes: %w", err) return nil, err
} }
ec2Client, err := state.EC2Client(catalogAttributes.Region) ec2Client, err := state.EC2Client(catalogAttributes.Region)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting EC2 client: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
} }
input, err := buildDescribeInstancesInput(setAttrs, true) input, err := buildDescribeInstancesInput(setAttrs, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("error building DescribeInstances parameters: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
} }
_, err = ec2Client.DescribeInstances(input) _, err = ec2Client.DescribeInstances(input)
if err == nil { if err == nil {
return nil, errors.New("query error: DescribeInstances DryRun should have returned error, but none was found") return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
} }
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" { if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
@@ -261,62 +263,62 @@ func (p *AwsPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest)
return &pb.OnCreateSetResponse{}, nil return &pb.OnCreateSetResponse{}, nil
} }
return nil, fmt.Errorf("error performing dry run of DescribeInstances: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
} }
func (p *AwsPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) { func (p *AwsPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
catalog := req.GetCatalog() catalog := req.GetCatalog()
if catalog == nil { if catalog == nil {
return nil, errors.New("catalog is nil") return nil, status.Error(codes.InvalidArgument, "catalog is nil")
} }
catalogAttrsRaw := catalog.GetAttributes() catalogAttrsRaw := catalog.GetAttributes()
if catalogAttrsRaw == nil { if catalogAttrsRaw == nil {
return nil, errors.New("catalog missing attributes") return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
} }
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw) catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
if err != nil { if err != nil {
return nil, fmt.Errorf("integrity error in host catalog attributes: %w", err) return nil, err
} }
if err := validateRegion(catalogAttributes.Region); err != nil { if err := validateRegion(catalogAttributes.Region); err != nil {
return nil, fmt.Errorf("integrity error in host catalog attributes: %w", err) return nil, err
} }
state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...) state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("error loading persisted state: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
} }
// As with catalog, we don't need to really look at the old host // As with catalog, we don't need to really look at the old host
// set here, just need to work off of/validate the new set config // set here, just need to work off of/validate the new set config
set := req.GetNewSet() set := req.GetNewSet()
if set == nil { if set == nil {
return nil, errors.New("new set is nil") return nil, status.Error(codes.InvalidArgument, "new set is nil")
} }
if set.GetAttributes() == nil { if set.GetAttributes() == nil {
return nil, errors.New("new set missing attributes") return nil, status.Error(codes.InvalidArgument, "new set missing attributes")
} }
setAttrs, err := getSetAttributes(set.GetAttributes()) setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing set attributes: %w", err) return nil, err
} }
ec2Client, err := state.EC2Client(catalogAttributes.Region) ec2Client, err := state.EC2Client(catalogAttributes.Region)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting EC2 client: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
} }
input, err := buildDescribeInstancesInput(setAttrs, true) input, err := buildDescribeInstancesInput(setAttrs, true)
if err != nil { if err != nil {
return nil, fmt.Errorf("error building DescribeInstances parameters: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters: %s", err)
} }
_, err = ec2Client.DescribeInstances(input) _, err = ec2Client.DescribeInstances(input)
if err == nil { if err == nil {
return nil, errors.New("query error: DescribeInstances DryRun should have returned error, but none was found") return nil, status.Error(codes.FailedPrecondition, "query error: DescribeInstances DryRun should have returned error, but none was found")
} }
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" { if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "DryRunOperation" {
@@ -324,7 +326,7 @@ func (p *AwsPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest)
return &pb.OnUpdateSetResponse{}, nil return &pb.OnUpdateSetResponse{}, nil
} }
return nil, fmt.Errorf("error performing dry run of DescribeInstances: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error performing dry run of DescribeInstances: %s", err)
} }
func (p *AwsPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest) (*pb.OnDeleteSetResponse, error) { func (p *AwsPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest) (*pb.OnDeleteSetResponse, error) {
@@ -336,31 +338,31 @@ func (p *AwsPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest)
func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) { func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
catalog := req.GetCatalog() catalog := req.GetCatalog()
if catalog == nil { if catalog == nil {
return nil, errors.New("catalog is nil") return nil, status.Error(codes.InvalidArgument, "catalog is nil")
} }
catalogAttrsRaw := catalog.GetAttributes() catalogAttrsRaw := catalog.GetAttributes()
if catalogAttrsRaw == nil { if catalogAttrsRaw == nil {
return nil, errors.New("catalog missing attributes") return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
} }
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw) catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
if err != nil { if err != nil {
return nil, fmt.Errorf("integrity error in host catalog attributes: %w", err) return nil, err
} }
if err := validateRegion(catalogAttributes.Region); err != nil { if err := validateRegion(catalogAttributes.Region); err != nil {
return nil, fmt.Errorf("integrity error in host catalog attributes: %w", err) return nil, err
} }
state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...) state, err := awsCatalogPersistedStateFromProto(req.GetPersisted(), p.testStateOpts...)
if err != nil { if err != nil {
return nil, fmt.Errorf("error loading persisted state: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
} }
sets := req.GetSets() sets := req.GetSets()
if sets == nil { if sets == nil {
return nil, errors.New("sets is nil") return nil, status.Error(codes.InvalidArgument, "sets is nil")
} }
// Build all the queries in advance. // Build all the queries in advance.
@@ -375,20 +377,20 @@ func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*p
for i, set := range sets { for i, set := range sets {
// Validate Id since we use it in output // Validate Id since we use it in output
if set.GetId() == "" { if set.GetId() == "" {
return nil, errors.New("set missing id") return nil, status.Error(codes.InvalidArgument, "set missing id")
} }
if set.GetAttributes() == nil { if set.GetAttributes() == nil {
return nil, errors.New("set missing attributes") return nil, status.Error(codes.InvalidArgument, "set missing attributes")
} }
setAttrs, err := getSetAttributes(set.GetAttributes()) setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil { if err != nil {
return nil, fmt.Errorf("error parsing set attributes: %w", err) return nil, err
} }
input, err := buildDescribeInstancesInput(setAttrs, false) input, err := buildDescribeInstancesInput(setAttrs, false)
if err != nil { if err != nil {
return nil, fmt.Errorf("error building DescribeInstances parameters for host set id %q: %w", set.GetId(), err) return nil, status.Errorf(codes.InvalidArgument, "error building DescribeInstances parameters for host set id %q: %s", set.GetId(), err)
} }
queries[i] = hostSetQuery{ queries[i] = hostSetQuery{
Id: set.GetId(), Id: set.GetId(),
@@ -398,7 +400,7 @@ func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*p
ec2Client, err := state.EC2Client(catalogAttributes.Region) ec2Client, err := state.EC2Client(catalogAttributes.Region)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting EC2 client: %w", err) return nil, status.Errorf(codes.InvalidArgument, "error getting EC2 client: %s", err)
} }
// Run all queries now and assemble output. // Run all queries now and assemble output.
@@ -406,7 +408,7 @@ func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*p
for i, query := range queries { for i, query := range queries {
output, err := ec2Client.DescribeInstances(query.Input) output, err := ec2Client.DescribeInstances(query.Input)
if err != nil { if err != nil {
return nil, fmt.Errorf("error running DescribeInstances for host set id %q: %w", query.Id, err) return nil, status.Errorf(codes.InvalidArgument, "error running DescribeInstances for host set id %q: %s", query.Id, err)
} }
queries[i].Output = output queries[i].Output = output
@@ -417,7 +419,7 @@ func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*p
for _, instance := range reservation.Instances { for _, instance := range reservation.Instances {
host, err := awsInstanceToHost(instance) host, err := awsInstanceToHost(instance)
if err != nil { if err != nil {
return nil, fmt.Errorf("error processing host results for host set id %q: %w", query.Id, err) return nil, status.Errorf(codes.InvalidArgument, "error processing host results for host set id %q: %s", query.Id, err)
} }
queries[i].OutputHosts = append(queries[i].OutputHosts, host) queries[i].OutputHosts = append(queries[i].OutputHosts, host)
@@ -461,7 +463,7 @@ func (p *AwsPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*p
func validateRegion(region string) error { func validateRegion(region string) error {
_, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region) _, found := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), region)
if !found { if !found {
return fmt.Errorf("not a valid region: %s", region) return status.Errorf(codes.InvalidArgument, "not a valid region: %s", region)
} }
return nil return nil
+164 -95
View File
@@ -14,27 +14,32 @@ import (
pb "github.com/hashicorp/boundary/sdk/pbs/plugin" pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/hashicorp/go-secure-stdlib/awsutil" "github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/structpb"
) )
func TestPluginOnCreateCatalogErr(t *testing.T) { func TestPluginOnCreateCatalogErr(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
req *pb.OnCreateCatalogRequest req *pb.OnCreateCatalogRequest
opts []awsCatalogPersistedStateOption opts []awsCatalogPersistedStateOption
expectedErr string expectedErrContains string
expectedErrCode codes.Code
}{ }{
{ {
name: "nil catalog", name: "nil catalog",
req: &pb.OnCreateCatalogRequest{}, req: &pb.OnCreateCatalogRequest{},
expectedErr: "catalog is nil", expectedErrContains: "catalog is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil secrets", name: "nil secrets",
req: &pb.OnCreateCatalogRequest{ req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{}, Catalog: &hostcatalogs.HostCatalog{},
}, },
expectedErr: "secrets are required", expectedErrContains: "secrets are required",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil attributes", name: "nil attributes",
@@ -43,7 +48,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Secrets: new(structpb.Struct), Secrets: new(structpb.Struct),
}, },
}, },
expectedErr: "attributes are required", expectedErrContains: "attributes are required",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "error reading attributes", name: "error reading attributes",
@@ -53,7 +59,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
Attributes: new(structpb.Struct), Attributes: new(structpb.Struct),
}, },
}, },
expectedErr: "missing required value \"region\"", expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "error reading secrets", name: "error reading secrets",
@@ -65,7 +72,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "missing required value \"access_key_id\"", expectedErrContains: "missing required value \"access_key_id\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "invalid region", name: "invalid region",
@@ -80,7 +88,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "not a valid region: foobar", expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "persisted state setup error", name: "persisted state setup error",
@@ -100,7 +109,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
return errors.New(testOptionErr) return errors.New(testOptionErr)
}, },
}, },
expectedErr: fmt.Sprintf("error setting up persisted state: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error setting up persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "rotation error", name: "rotation error",
@@ -124,7 +134,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
), ),
}), }),
}, },
expectedErr: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "validation error", name: "validation error",
@@ -149,7 +160,8 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
), ),
}), }),
}, },
expectedErr: fmt.Sprintf("error during credential validation: error validating credentials: %s", testGetCallerIdentityErr), expectedErrContains: fmt.Sprintf("error during credential validation: error validating credentials: %s", testGetCallerIdentityErr),
expectedErrCode: codes.InvalidArgument,
}, },
} }
@@ -161,22 +173,25 @@ func TestPluginOnCreateCatalogErr(t *testing.T) {
testStateOpts: tc.opts, testStateOpts: tc.opts,
} }
_, err := p.OnCreateCatalog(context.Background(), tc.req) _, err := p.OnCreateCatalog(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
}) })
} }
} }
func TestPluginOnUpdateCatalogErr(t *testing.T) { func TestPluginOnUpdateCatalogErr(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
req *pb.OnUpdateCatalogRequest req *pb.OnUpdateCatalogRequest
opts []awsCatalogPersistedStateOption opts []awsCatalogPersistedStateOption
expectedErr string expectedErrContains string
expectedErrCode codes.Code
}{ }{
{ {
name: "nil new catalog", name: "nil new catalog",
req: &pb.OnUpdateCatalogRequest{}, req: &pb.OnUpdateCatalogRequest{},
expectedErr: "new catalog is nil", expectedErrContains: "new catalog is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil new attributes", name: "nil new attributes",
@@ -185,7 +200,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
Secrets: new(structpb.Struct), Secrets: new(structpb.Struct),
}, },
}, },
expectedErr: "new catalog missing attributes", expectedErrContains: "new catalog missing attributes",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "error reading attributes", name: "error reading attributes",
@@ -195,7 +211,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
Attributes: new(structpb.Struct), Attributes: new(structpb.Struct),
}, },
}, },
expectedErr: "missing required value \"region\"", expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "invalid region", name: "invalid region",
@@ -206,7 +223,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "not a valid region: foobar", expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "persisted state setup error", name: "persisted state setup error",
@@ -229,7 +247,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
return errors.New(testOptionErr) return errors.New(testOptionErr)
}, },
}, },
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "cannot disable rotation for already rotated credentials", name: "cannot disable rotation for already rotated credentials",
@@ -248,7 +267,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "cannot disable rotation for already-rotated credentials", expectedErrContains: "cannot disable rotation for already-rotated credentials",
expectedErrCode: codes.FailedPrecondition,
}, },
{ {
name: "error reading secrets", name: "error reading secrets",
@@ -267,7 +287,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "missing required value \"access_key_id\"", expectedErrContains: "missing required value \"access_key_id\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "updating secrets, replace error", name: "updating secrets, replace error",
@@ -299,7 +320,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
), ),
}), }),
}, },
expectedErr: fmt.Sprintf("error attempting to replace credentials: error deleting old access key: %s", testDeleteAccessKeyErr), expectedErrContains: fmt.Sprintf("error attempting to replace credentials: error deleting old access key: %s", testDeleteAccessKeyErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "rotation error", name: "rotation error",
@@ -325,7 +347,8 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
), ),
}), }),
}, },
expectedErr: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr), expectedErrContains: fmt.Sprintf("error during credential rotation: error rotating credentials: error calling CreateAccessKey: error calling aws.GetUser: %s", testGetUserErr),
expectedErrCode: codes.InvalidArgument,
}, },
} }
@@ -337,17 +360,19 @@ func TestPluginOnUpdateCatalogErr(t *testing.T) {
testStateOpts: tc.opts, testStateOpts: tc.opts,
} }
_, err := p.OnUpdateCatalog(context.Background(), tc.req) _, err := p.OnUpdateCatalog(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
}) })
} }
} }
func TestPluginOnDeleteCatalogErr(t *testing.T) { func TestPluginOnDeleteCatalogErr(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
req *pb.OnDeleteCatalogRequest req *pb.OnDeleteCatalogRequest
opts []awsCatalogPersistedStateOption opts []awsCatalogPersistedStateOption
expectedErr string expectedErrContains string
expectedErrCode codes.Code
}{ }{
{ {
name: "persisted state setup error", name: "persisted state setup error",
@@ -370,7 +395,8 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
return errors.New(testOptionErr) return errors.New(testOptionErr)
}, },
}, },
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "delete error", name: "delete error",
@@ -397,7 +423,8 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
), ),
}), }),
}, },
expectedErr: fmt.Sprintf("error removing rotated credentials during catalog deletion: error deleting old access key: %s", testDeleteAccessKeyErr), expectedErrContains: fmt.Sprintf("error removing rotated credentials during catalog deletion: error deleting old access key: %s", testDeleteAccessKeyErr),
expectedErrCode: codes.Aborted,
}, },
} }
@@ -409,22 +436,25 @@ func TestPluginOnDeleteCatalogErr(t *testing.T) {
testStateOpts: tc.opts, testStateOpts: tc.opts,
} }
_, err := p.OnDeleteCatalog(context.Background(), tc.req) _, err := p.OnDeleteCatalog(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
}) })
} }
} }
func TestPluginOnCreateSetErr(t *testing.T) { func TestPluginOnCreateSetErr(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
req *pb.OnCreateSetRequest req *pb.OnCreateSetRequest
opts []awsCatalogPersistedStateOption opts []awsCatalogPersistedStateOption
expectedErr string expectedErrContains string
expectedErrCode codes.Code
}{ }{
{ {
name: "nil catalog", name: "nil catalog",
req: &pb.OnCreateSetRequest{}, req: &pb.OnCreateSetRequest{},
expectedErr: "catalog is nil", expectedErrContains: "catalog is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil catalog attributes", name: "nil catalog attributes",
@@ -433,7 +463,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Secrets: new(structpb.Struct), Secrets: new(structpb.Struct),
}, },
}, },
expectedErr: "catalog missing attributes", expectedErrContains: "catalog missing attributes",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "error reading catalog attributes", name: "error reading catalog attributes",
@@ -443,7 +474,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
Attributes: new(structpb.Struct), Attributes: new(structpb.Struct),
}, },
}, },
expectedErr: "integrity error in host catalog attributes: missing required value \"region\"", expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "invalid region", name: "invalid region",
@@ -454,7 +486,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "integrity error in host catalog attributes: not a valid region: foobar", expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "persisted state setup error", name: "persisted state setup error",
@@ -477,7 +510,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
return errors.New(testOptionErr) return errors.New(testOptionErr)
}, },
}, },
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil set", name: "nil set",
@@ -495,7 +529,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "set is nil", expectedErrContains: "set is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil attributes in set", name: "nil attributes in set",
@@ -514,7 +549,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
}, },
Set: &hostsets.HostSet{}, Set: &hostsets.HostSet{},
}, },
expectedErr: "set missing attributes", expectedErrContains: "set missing attributes",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "set attribute load error", name: "set attribute load error",
@@ -538,7 +574,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "error parsing set attributes: unknown set attribute fields provided: bar, foo", expectedErrContains: "attributes.bar: Unrecognized field, attributes.foo: Unrecognized field",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "client load error", name: "client load error",
@@ -566,7 +603,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
awsutil.MockOptionErr(errors.New(testOptionErr)), awsutil.MockOptionErr(errors.New(testOptionErr)),
}), }),
}, },
expectedErr: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "DescribeInstances error", name: "DescribeInstances error",
@@ -595,7 +633,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)), testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)), )),
}, },
expectedErr: fmt.Sprintf("error performing dry run of DescribeInstances: %s", testDescribeInstancesError), expectedErrContains: fmt.Sprintf("error performing dry run of DescribeInstances: %s", testDescribeInstancesError),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "DescribeInstances non-error array filter", name: "DescribeInstances non-error array filter",
@@ -623,7 +662,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
nil, nil,
)), )),
}, },
expectedErr: "query error: DescribeInstances DryRun should have returned error, but none was found", expectedErrContains: "query error: DescribeInstances DryRun should have returned error, but none was found",
expectedErrCode: codes.FailedPrecondition,
}, },
{ {
name: "DescribeInstances non-error string filter", name: "DescribeInstances non-error string filter",
@@ -651,7 +691,8 @@ func TestPluginOnCreateSetErr(t *testing.T) {
nil, nil,
)), )),
}, },
expectedErr: "query error: DescribeInstances DryRun should have returned error, but none was found", expectedErrContains: "query error: DescribeInstances DryRun should have returned error, but none was found",
expectedErrCode: codes.FailedPrecondition,
}, },
} }
@@ -663,22 +704,25 @@ func TestPluginOnCreateSetErr(t *testing.T) {
testStateOpts: tc.opts, testStateOpts: tc.opts,
} }
_, err := p.OnCreateSet(context.Background(), tc.req) _, err := p.OnCreateSet(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
}) })
} }
} }
func TestPluginOnUpdateSetErr(t *testing.T) { func TestPluginOnUpdateSetErr(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
req *pb.OnUpdateSetRequest req *pb.OnUpdateSetRequest
opts []awsCatalogPersistedStateOption opts []awsCatalogPersistedStateOption
expectedErr string expectedErrContains string
expectedErrCode codes.Code
}{ }{
{ {
name: "nil catalog", name: "nil catalog",
req: &pb.OnUpdateSetRequest{}, req: &pb.OnUpdateSetRequest{},
expectedErr: "catalog is nil", expectedErrContains: "catalog is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil catalog attributes", name: "nil catalog attributes",
@@ -687,7 +731,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Secrets: new(structpb.Struct), Secrets: new(structpb.Struct),
}, },
}, },
expectedErr: "catalog missing attributes", expectedErrContains: "catalog missing attributes",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "catalog attribute load error", name: "catalog attribute load error",
@@ -697,7 +742,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
Attributes: new(structpb.Struct), Attributes: new(structpb.Struct),
}, },
}, },
expectedErr: "integrity error in host catalog attributes: missing required value \"region\"", expectedErrContains: "missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "invalid region", name: "invalid region",
@@ -709,7 +755,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "integrity error in host catalog attributes: not a valid region: foobar", expectedErrContains: "not a valid region: foobar",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "persisted state setup error", name: "persisted state setup error",
@@ -732,7 +779,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
return errors.New(testOptionErr) return errors.New(testOptionErr)
}, },
}, },
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil set", name: "nil set",
@@ -750,7 +798,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "new set is nil", expectedErrContains: "new set is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil attributes in set", name: "nil attributes in set",
@@ -769,7 +818,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
}, },
NewSet: &hostsets.HostSet{}, NewSet: &hostsets.HostSet{},
}, },
expectedErr: "new set missing attributes", expectedErrContains: "new set missing attributes",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "set attribute load error", name: "set attribute load error",
@@ -793,7 +843,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "error parsing set attributes: unknown set attribute fields provided: bar, foo", expectedErrContains: "attributes.bar: Unrecognized field, attributes.foo: Unrecognized field",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "client load error", name: "client load error",
@@ -821,7 +872,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
awsutil.MockOptionErr(errors.New(testOptionErr)), awsutil.MockOptionErr(errors.New(testOptionErr)),
}), }),
}, },
expectedErr: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "DescribeInstances error", name: "DescribeInstances error",
@@ -850,7 +902,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)), testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)), )),
}, },
expectedErr: fmt.Sprintf("error performing dry run of DescribeInstances: %s", testDescribeInstancesError), expectedErrContains: fmt.Sprintf("error performing dry run of DescribeInstances: %s", testDescribeInstancesError),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "DescribeInstances non-error", name: "DescribeInstances non-error",
@@ -878,7 +931,8 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
nil, nil,
)), )),
}, },
expectedErr: "query error: DescribeInstances DryRun should have returned error, but none was found", expectedErrContains: "query error: DescribeInstances DryRun should have returned error, but none was found",
expectedErrCode: codes.FailedPrecondition,
}, },
} }
@@ -890,22 +944,25 @@ func TestPluginOnUpdateSetErr(t *testing.T) {
testStateOpts: tc.opts, testStateOpts: tc.opts,
} }
_, err := p.OnUpdateSet(context.Background(), tc.req) _, err := p.OnUpdateSet(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
}) })
} }
} }
func TestPluginListHostsErr(t *testing.T) { func TestPluginListHostsErr(t *testing.T) {
cases := []struct { cases := []struct {
name string name string
req *pb.ListHostsRequest req *pb.ListHostsRequest
opts []awsCatalogPersistedStateOption opts []awsCatalogPersistedStateOption
expectedErr string expectedErrContains string
expectedErrCode codes.Code
}{ }{
{ {
name: "nil catalog", name: "nil catalog",
req: &pb.ListHostsRequest{}, req: &pb.ListHostsRequest{},
expectedErr: "catalog is nil", expectedErrContains: "catalog is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil catalog attributes", name: "nil catalog attributes",
@@ -914,7 +971,8 @@ func TestPluginListHostsErr(t *testing.T) {
Secrets: new(structpb.Struct), Secrets: new(structpb.Struct),
}, },
}, },
expectedErr: "catalog missing attributes", expectedErrContains: "catalog missing attributes",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "error reading catalog attributes", name: "error reading catalog attributes",
@@ -924,7 +982,8 @@ func TestPluginListHostsErr(t *testing.T) {
Attributes: new(structpb.Struct), Attributes: new(structpb.Struct),
}, },
}, },
expectedErr: "integrity error in host catalog attributes: missing required value \"region\"", expectedErrContains: "attributes.region: missing required value \"region\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "invalid region", name: "invalid region",
@@ -936,7 +995,8 @@ func TestPluginListHostsErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "integrity error in host catalog attributes: not a valid region: foobar", expectedErrContains: "not a valid region: ",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "persisted state setup error", name: "persisted state setup error",
@@ -959,7 +1019,8 @@ func TestPluginListHostsErr(t *testing.T) {
return errors.New(testOptionErr) return errors.New(testOptionErr)
}, },
}, },
expectedErr: fmt.Sprintf("error loading persisted state: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error loading persisted state: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil sets", name: "nil sets",
@@ -977,7 +1038,8 @@ func TestPluginListHostsErr(t *testing.T) {
}), }),
}, },
}, },
expectedErr: "sets is nil", expectedErrContains: "sets is nil",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "set missing id", name: "set missing id",
@@ -996,7 +1058,8 @@ func TestPluginListHostsErr(t *testing.T) {
}, },
Sets: []*hostsets.HostSet{&hostsets.HostSet{}}, Sets: []*hostsets.HostSet{&hostsets.HostSet{}},
}, },
expectedErr: "set missing id", expectedErrContains: "set missing id",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "set missing attributes", name: "set missing attributes",
@@ -1019,7 +1082,8 @@ func TestPluginListHostsErr(t *testing.T) {
}, },
}, },
}, },
expectedErr: "set missing attributes", expectedErrContains: "set missing attributes",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "set attribute load error", name: "set attribute load error",
@@ -1046,7 +1110,8 @@ func TestPluginListHostsErr(t *testing.T) {
}, },
}, },
}, },
expectedErr: "error parsing set attributes: unknown set attribute fields provided: bar, foo", expectedErrContains: "attributes.bar: Unrecognized field, attributes.foo: Unrecognized field",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "client load error", name: "client load error",
@@ -1077,7 +1142,8 @@ func TestPluginListHostsErr(t *testing.T) {
awsutil.MockOptionErr(errors.New(testOptionErr)), awsutil.MockOptionErr(errors.New(testOptionErr)),
}), }),
}, },
expectedErr: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr), expectedErrContains: fmt.Sprintf("error getting EC2 client: error getting AWS session: error reading options in NewCredentialsConfig: %s", testOptionErr),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "DescribeInstances error", name: "DescribeInstances error",
@@ -1109,7 +1175,8 @@ func TestPluginListHostsErr(t *testing.T) {
testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)), testMockEC2WithDescribeInstancesError(errors.New(testDescribeInstancesError)),
)), )),
}, },
expectedErr: fmt.Sprintf("error running DescribeInstances for host set id \"foobar\": %s", testDescribeInstancesError), expectedErrContains: fmt.Sprintf("error running DescribeInstances for host set id \"foobar\": %s", testDescribeInstancesError),
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "awsInstanceToHost error", name: "awsInstanceToHost error",
@@ -1153,7 +1220,8 @@ func TestPluginListHostsErr(t *testing.T) {
), ),
)), )),
}, },
expectedErr: "error processing host results for host set id \"foobar\": response integrity error: missing instance id", expectedErrContains: "error processing host results for host set id \"foobar\": response integrity error: missing instance id",
expectedErrCode: codes.InvalidArgument,
}, },
} }
@@ -1165,7 +1233,8 @@ func TestPluginListHostsErr(t *testing.T) {
testStateOpts: tc.opts, testStateOpts: tc.opts,
} }
_, err := p.ListHosts(context.Background(), tc.req) _, err := p.ListHosts(context.Background(), tc.req)
require.EqualError(err, tc.expectedErr) require.Contains(err.Error(), tc.expectedErrContains)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
}) })
} }
} }