Compare commits

..
Author SHA1 Message Date
Todd 1a91668823 Update documentation to cover more filter options as provided by AWS. 2022-01-20 16:35:32 -07:00
5 changed files with 167 additions and 271 deletions

No files matched your search

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