feat: onCreateSet

Add support for onCreateSet with validation.
- If a filter is not provided or a status is not set, a default filter of `status = running` is used.
- update `filters` field to be a slice instead of a string
- add dryRunValidation call to validate fields
This commit is contained in:
Elim Tsiagbey committed 2024-10-28 13:09:23 -04:00
1 parent beaa93d3d8
commit e4099a5f64
6 files changed
+451 -59

No files matched your search

+10 -7
View File
@@ -5,6 +5,7 @@ package host
import ( import (
"fmt" "fmt"
"strings"
computepb "cloud.google.com/go/compute/apiv1/computepb" computepb "cloud.google.com/go/compute/apiv1/computepb"
cred "github.com/hashicorp/boundary-plugin-gcp/internal/credential" cred "github.com/hashicorp/boundary-plugin-gcp/internal/credential"
@@ -60,8 +61,8 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
// SetAttributes defines attributes fro the host set // SetAttributes defines attributes fro the host set
type SetAttributes struct { type SetAttributes struct {
Filter string `mapstructure:"filter"` Filters []string `mapstructure:"filters"`
InstanceGroup string `mapstructure:"instance_group"` InstanceGroup string `mapstructure:"instance_group"`
} }
func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) { func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
@@ -93,7 +94,7 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
if filtersRaw, ok := inMap[ConstListInstancesFilter]; ok { if filtersRaw, ok := inMap[ConstListInstancesFilter]; ok {
switch filterVal := filtersRaw.(type) { switch filterVal := filtersRaw.(type) {
case string: case string:
inMap[ConstListInstancesFilter] = string(filterVal) inMap[ConstListInstancesFilter] = []string{filterVal}
} }
} }
@@ -117,8 +118,9 @@ func buildListInstancesRequest(attributes *SetAttributes, catalog *CatalogAttrib
Zone: catalog.Zone, Zone: catalog.Zone,
} }
if len(attributes.Filter) > 1 { if len(attributes.Filters) > 1 {
request.Filter = &attributes.Filter filters := strings.Join(attributes.Filters, " ")
request.Filter = &filters
} }
return request return request
@@ -131,8 +133,9 @@ func buildListInstanceGroupsRequest(attributes *SetAttributes, catalog *CatalogA
Zone: catalog.Zone, Zone: catalog.Zone,
} }
if len(attributes.Filter) > 1 { if len(attributes.Filters) > 1 {
request.Filter = &attributes.Filter filters := strings.Join(attributes.Filters, " ")
request.Filter = &filters
} }
return request return request
+1 -1
View File
@@ -92,7 +92,7 @@ func TestGetSetAttributes(t *testing.T) {
ConstListInstancesFilter: "tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag", ConstListInstancesFilter: "tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag",
}, },
expected: &SetAttributes{ expected: &SetAttributes{
Filter: "tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag", Filters: []string{"tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag"},
}, },
}, },
{ {
+1 -1
View File
@@ -5,7 +5,7 @@ package host
const ( const (
// ConstListInstancesFilter refers to a Google Cloud SDK filter to search for instances // ConstListInstancesFilter refers to a Google Cloud SDK filter to search for instances
ConstListInstancesFilter = "filter" ConstListInstancesFilter = "filters"
// ConstInstanceGroup refers to the name of a Google Cloud instance group // ConstInstanceGroup refers to the name of a Google Cloud instance group
ConstInstanceGroup = "instance_group" ConstInstanceGroup = "instance_group"
+33
View File
@@ -0,0 +1,33 @@
package host
// Package host provides a plugin for Boundary that retrieves host information from (GCP) Google Cloud Platform.
//
// A host catalog is a resource that contains hosts and host sets.
//
// A host is a resource that represents a computing element with a network address reachable from Boundary.
// A host belongs to a host catalog. Hosts can only be associated with host sets from the same host catalog as the host.
//
// A host set is a resource that represents a collection of hosts which are considered equivalent for the purposes of access control.
//
// The plugin supports authentication using GCP service account keys, impersonation of service accounts
// and Application Default Credentials.
//
// Host plugin supports credential rotation for GCP service account keys and impersonation of service accounts (only for base account)
// when the `disable_credential_rotation` attribute is set to false.
//
// The plugin implements Boundary's HostPluginService interface:
// https://github.com/hashicorp/boundary-enterprise/blob/cb9ce07d6907cbda6ce82e82627de372f18ba7e1/internal/proto/plugin/v1/host_plugin_service.proto#L15.
//
// # Filters
// Host Set filters are used to narrow down the list of hosts returned by the plugin.
// The filter string is expected to be in the format "key operator value".
// The operator is expected to be one of =, !=, >, <, <=, >=, :, eq, ne.
// as per GCP API documentation:
// https://cloud.google.com/compute/docs/reference/rest/v1/instances/list#filter
//
// If there is no explicit instance status filter, the default status will be set to
// `status = "running"`. This ensures that we filter on running instances only,
// saving time when processing results.
//
// Host Set filters are represented as a list of strings. When multiple filters are provided,
// the plugin will apply a logical `AND` operation to the filters.
+137 -8
View File
@@ -6,6 +6,8 @@ package host
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings"
compute "cloud.google.com/go/compute/apiv1" compute "cloud.google.com/go/compute/apiv1"
computepb "cloud.google.com/go/compute/apiv1/computepb" computepb "cloud.google.com/go/compute/apiv1/computepb"
@@ -36,6 +38,13 @@ var (
_ pb.HostPluginServiceServer = (*HostPlugin)(nil) _ pb.HostPluginServiceServer = (*HostPlugin)(nil)
) )
const (
// defaultStatusFilter is the default filter to apply to instances
// if no status filter is provided.
// This is used to filter on running instances only.
defaultStatusFilter = "status = running"
)
// OnCreateCatalog is called when a dynamic host catalog is created. // OnCreateCatalog is called when a dynamic host catalog is created.
func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) { func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
catalog := req.GetCatalog() catalog := req.GetCatalog()
@@ -87,7 +96,7 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err) return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err)
} }
if st := dryRunValidation(ctx, catalogState, p.testGCPClientOpts...); st != nil { if st := dryRunValidation(ctx, catalogState, nil, p.testGCPClientOpts...); st != nil {
return nil, st.Err() return nil, st.Err()
} }
@@ -158,7 +167,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
if err != nil { if err != nil {
return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err) return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err)
} }
if st := dryRunValidation(ctx, newCatalogState); st != nil { if st := dryRunValidation(ctx, newCatalogState, nil, p.testGCPClientOpts...); st != nil {
return nil, st.Err() return nil, st.Err()
} }
@@ -210,7 +219,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
} }
// perform dry run to ensure we can interact with GCP as expected. // perform dry run to ensure we can interact with GCP as expected.
if st := dryRunValidation(ctx, catalogState, p.testGCPClientOpts...); st != nil { if st := dryRunValidation(ctx, catalogState, nil, p.testGCPClientOpts...); st != nil {
return nil, st.Err() return nil, st.Err()
} }
@@ -309,10 +318,64 @@ func (p *HostPlugin) NormalizeSetData(ctx context.Context, req *pb.NormalizeSetD
} }
// OnCreateSet is called when a dynamic host set is created. // OnCreateSet is called when a dynamic host set is created.
func (p *HostPlugin) OnCreateSet(_ context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) { func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
catalog := req.GetCatalog()
if catalog == nil {
return nil, status.Error(codes.InvalidArgument, "catalog is required")
}
attrs := catalog.GetAttributes()
if attrs == nil {
return nil, status.Error(codes.InvalidArgument, "catalog attributes are required")
}
catalogAttributes, err := getCatalogAttributes(attrs)
if err != nil {
return nil, err
}
credState, err := credential.PersistedStateFromProto(
req.GetPersisted().GetSecrets(),
catalogAttributes.CredentialAttributes)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error getting persisted state from proto: %s", err)
}
catalogState, err := newGCPCatalogPersistedState(
append([]gcpCatalogPersistedStateOption{
withCredentials(credState),
}, p.testCatalogStateOpts...)...,
)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
}
set := req.GetSet()
if set == nil {
return nil, status.Error(codes.InvalidArgument, "set is required")
}
if err := validateSet(req.GetSet()); err != nil { if err := validateSet(req.GetSet()); err != nil {
return nil, err return nil, err
} }
if set.GetAttributes() == nil {
return nil, status.Error(codes.InvalidArgument, "set attributes are required")
}
setAttrs, err := getSetAttributes(set.GetAttributes())
if err != nil {
return nil, err
}
listInstanceFilters, err := buildFilters(setAttrs)
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "error building filters: %s", err)
}
// perform dry run to ensure we can interact with GCP as expected.
if st := dryRunValidation(ctx, catalogState, listInstanceFilters, p.testGCPClientOpts...); st != nil {
return nil, st.Err()
}
return &pb.OnCreateSetResponse{}, nil return &pb.OnCreateSetResponse{}, nil
} }
@@ -485,14 +548,14 @@ func validateSet(s *hostsets.HostSet) error {
if instanceGroupSet && filterSet { if instanceGroupSet && filterSet {
badFields["attributes"] = "must set instance group or filter, cannot set both" badFields["attributes"] = "must set instance group or filter, cannot set both"
} else if instanceGroupSet && len(attrs.InstanceGroup) == 0 { } else if instanceGroupSet && len(attrs.InstanceGroup) == 0 {
badFields[fmt.Sprintf("attributes.%s", ConstInstanceGroup)] = "must not be empty." badFields[fmt.Sprintf("attributes.%s", ConstInstanceGroup)] = "must not be empty"
} else if filterSet && len(attrs.Filter) == 0 { } else if filterSet && len(attrs.Filters) == 0 {
badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty." badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty"
} }
for f := range attrMap { for f := range attrMap {
if _, ok := allowedSetFields[f]; !ok { if _, ok := allowedSetFields[f]; !ok {
badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field." badFields[fmt.Sprintf("attributes.%s", f)] = "unrecognized field"
} }
} }
@@ -507,6 +570,7 @@ func validateSet(s *hostsets.HostSet) error {
func dryRunValidation( func dryRunValidation(
ctx context.Context, ctx context.Context,
state *gcpCatalogPersistedState, state *gcpCatalogPersistedState,
listFilters []string,
clientOptions ...option.ClientOption, clientOptions ...option.ClientOption,
) *status.Status { ) *status.Status {
if state == nil { if state == nil {
@@ -518,9 +582,15 @@ func dryRunValidation(
return status.New(codes.InvalidArgument, fmt.Sprintf("error getting instances client: %s", err)) return status.New(codes.InvalidArgument, fmt.Sprintf("error getting instances client: %s", err))
} }
var filters string
if len(listFilters) > 0 {
filters = strings.Join(listFilters, " ")
}
it := instancesClient.List(ctx, &computepb.ListInstancesRequest{ it := instancesClient.List(ctx, &computepb.ListInstancesRequest{
Project: state.CredentialsConfig.ProjectId, Project: state.CredentialsConfig.ProjectId,
Zone: state.CredentialsConfig.Zone, Zone: state.CredentialsConfig.Zone,
Filter: &filters,
}) })
_, err = it.Next() _, err = it.Next()
@@ -530,3 +600,62 @@ func dryRunValidation(
return nil return nil
} }
// buildFilters constructs a list of filters to be used in a ListInstancesRequest.
// If no filters are provided, a default filter is added to filter on running instances.
func buildFilters(attrs *SetAttributes) ([]string, error) {
var filters []string
var foundStateFilter bool
// If filters are provided, validate them
if len(attrs.Filters) != 0 {
for _, filterAttr := range attrs.Filters {
key, operator, value, valid := extractFilterValue(filterAttr)
switch {
case !valid:
return nil, fmt.Errorf("filter %q contains invalid operator: %s. Use one of =, !=, >, <, <=, >=, :, eq, ne", filterAttr, operator)
case len(strings.TrimSpace(key)) == 0:
return nil, fmt.Errorf("filter %q contains an empty filter key", filterAttr)
case len(strings.TrimSpace(value)) == 0:
return nil, fmt.Errorf("filter %q contains an empty value", filterAttr)
}
if key == "status" {
foundStateFilter = true
}
filters = append(filters, filterAttr)
}
}
if !foundStateFilter {
// if there is no explicit instance state filter, add the
// status = "running" to the filter set. This
// ensures that we filter on running instances only at the API
// side, saving time when processing results.
filters = append(filters, defaultStatusFilter)
}
return filters, nil
}
// extractFilterValue extracts the key and value from a filter string.
// The filter string is expected to be in the format "key operator value".
// The key and value are returned as strings, along with a boolean indicating
// whether the extraction was successful.
// The operator is expected to be one of =, !=, >, <, <=, >=, :, eq, ne.
// as per GCP API documentation:
// https://cloud.google.com/compute/docs/reference/rest/v1/instances/list#filter
func extractFilterValue(s string) (string, string, string, bool) {
re := regexp.MustCompile(`(?i)([^=><!:\s]+)\s*(=|!=|>|<|<=|>=|:|eq|ne)\s*([^,]+)`)
matches := re.FindStringSubmatch(s)
// Check if we have the expected number of matches
if len(matches) == 4 {
key := matches[1]
operator := matches[2]
value := matches[3]
return key, operator, value, true
}
return "", "", "", false
}
+269 -42
View File
@@ -6,6 +6,7 @@ package host
import ( import (
"context" "context"
"fmt" "fmt"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -949,17 +950,41 @@ func TestDeleteCatalog(t *testing.T) {
func TestCreateSet(t *testing.T) { func TestCreateSet(t *testing.T) {
ctx := context.Background() ctx := context.Background()
p := &HostPlugin{}
cases := []struct { cases := []struct {
name string name string
req *pb.OnCreateSetRequest req *pb.OnCreateSetRequest
expectedErr string catalogOpts []gcpCatalogPersistedStateOption
gcpOptions []option.ClientOption
expectedErr string
expectedErrCode codes.Code
}{ }{
{ {
name: "nil set", name: "nil host catalog",
req: &pb.OnCreateSetRequest{}, req: &pb.OnCreateSetRequest{},
expectedErr: "set is nil", expectedErr: "catalog is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil host catalog attributes",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{},
},
expectedErr: "catalog attributes are required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading catalog attributes",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErr: "missing required value \"project_id\"",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "nil set", name: "nil set",
@@ -975,80 +1000,270 @@ func TestCreateSet(t *testing.T) {
}, },
}, },
}, },
expectedErr: "set is nil", expectedErr: "set is required",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "only allow instance group or filter, not both", name: "persisted state setup error",
req: &pb.OnCreateSetRequest{ req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
func(s *gcpCatalogPersistedState) error {
return fmt.Errorf("error loading persisted state")
},
},
expectedErr: "error loading persisted state",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil attributes in set",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
Set: &hostsets.HostSet{},
},
expectedErr: "set attributes are required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "set attribute load error",
req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
Set: &hostsets.HostSet{ Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{ Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
ConstListInstancesFilter: structpb.NewStringValue("status=RUNNING"), "foo": structpb.NewBoolValue(true),
ConstInstanceGroup: structpb.NewStringValue("test"), "bar": structpb.NewBoolValue(true),
}, },
}, },
}, },
}, },
}, },
expectedErr: "attributes: must set instance group or filter", expectedErr: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "empty filter", name: "invalid filter",
req: &pb.OnCreateSetRequest{ req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
Set: &hostsets.HostSet{ Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{ Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
ConstListInstancesFilter: structpb.NewStringValue(""), ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("invalid"),
},
},
),
}, },
}, },
}, },
}, },
}, },
expectedErr: "attributes.filter: must not be empty", expectedErr: "error building filters",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "empty instance group", name: "client load error",
req: &pb.OnCreateSetRequest{ req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
Set: &hostsets.HostSet{ Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{ Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
ConstInstanceGroup: structpb.NewStringValue(""), ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
}, },
}, },
}, },
}, },
}, },
expectedErr: "attributes.instance_group: must not be empty", gcpOptions: []option.ClientOption{
option.WithHTTPClient(&http.Client{}),
},
expectedErr: "error getting instances client",
expectedErrCode: codes.InvalidArgument,
}, },
{ {
name: "good filter", name: "List instances error",
req: &pb.OnCreateSetRequest{ req: &pb.OnCreateSetRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
Set: &hostsets.HostSet{ Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{ Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
ConstInstanceGroup: structpb.NewStringValue("status=RUNNING"), ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
}, },
}, },
}, },
}, },
}, },
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesError(fmt.Errorf("failed to list instances")),
)),
},
expectedErr: "gcp list instances failed",
expectedErrCode: codes.FailedPrecondition,
}, },
{ {
name: "good instance group", name: "success",
req: &pb.OnCreateSetRequest{ req: &pb.OnCreateSetRequest{
Set: &hostsets.HostSet{ Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{ Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
ConstInstanceGroup: structpb.NewStringValue("test"), cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
}, },
}, },
}, },
}, },
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue("2006-01-02T15:04:05+07:00"),
},
},
},
Set: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil)),
),
}, },
}, },
} }
@@ -1058,9 +1273,30 @@ func TestCreateSet(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
require := require.New(t) require := require.New(t)
_, err := p.OnCreateSet(ctx, tc.req) testIAMAdminServer := cred.NewTestIAMAdminServer(nil, nil)
gsrv := cred.NewGRPCServer()
adminpb.RegisterIAMServer(gsrv.Server, testIAMAdminServer)
addr, err := gsrv.Start()
require.NoError(err)
clientOptions := []option.ClientOption{
option.WithEndpoint(addr),
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
option.WithTokenSource(nil),
}
clientOptions = append(clientOptions, tc.gcpOptions...)
p := &HostPlugin{
testGCPClientOpts: clientOptions,
testCatalogStateOpts: tc.catalogOpts,
}
_, err = p.OnCreateSet(ctx, tc.req)
if tc.expectedErr != "" { if tc.expectedErr != "" {
require.Contains(err.Error(), tc.expectedErr) require.Contains(err.Error(), tc.expectedErr)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return return
} }
@@ -1106,8 +1342,14 @@ func TestUpdateSet(t *testing.T) {
Attrs: &hostsets.HostSet_Attributes{ Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{ Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{ Fields: map[string]*structpb.Value{
ConstListInstancesFilter: structpb.NewStringValue("status=RUNNING"), ConstListInstancesFilter: structpb.NewListValue(
ConstInstanceGroup: structpb.NewStringValue("test"), &structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("status=RUNNING"),
},
},
),
ConstInstanceGroup: structpb.NewStringValue("test"),
}, },
}, },
}, },
@@ -1115,21 +1357,6 @@ func TestUpdateSet(t *testing.T) {
}, },
expectedErr: "attributes: must set instance group or filter", expectedErr: "attributes: must set instance group or filter",
}, },
{
name: "empty filter",
req: &pb.OnUpdateSetRequest{
NewSet: &hostsets.HostSet{
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstListInstancesFilter: structpb.NewStringValue(""),
},
},
},
},
},
expectedErr: "attributes.filter: must not be empty",
},
{ {
name: "empty instance group", name: "empty instance group",
req: &pb.OnUpdateSetRequest{ req: &pb.OnUpdateSetRequest{