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:
1 parent
beaa93d3d8
commit
e4099a5f64
6 files changed
+451
-59
No files matched your search
@@ -5,6 +5,7 @@ package host
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||
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
|
||||
type SetAttributes struct {
|
||||
Filter string `mapstructure:"filter"`
|
||||
InstanceGroup string `mapstructure:"instance_group"`
|
||||
Filters []string `mapstructure:"filters"`
|
||||
InstanceGroup string `mapstructure:"instance_group"`
|
||||
}
|
||||
|
||||
func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
||||
@@ -93,7 +94,7 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
||||
if filtersRaw, ok := inMap[ConstListInstancesFilter]; ok {
|
||||
switch filterVal := filtersRaw.(type) {
|
||||
case string:
|
||||
inMap[ConstListInstancesFilter] = string(filterVal)
|
||||
inMap[ConstListInstancesFilter] = []string{filterVal}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +118,9 @@ func buildListInstancesRequest(attributes *SetAttributes, catalog *CatalogAttrib
|
||||
Zone: catalog.Zone,
|
||||
}
|
||||
|
||||
if len(attributes.Filter) > 1 {
|
||||
request.Filter = &attributes.Filter
|
||||
if len(attributes.Filters) > 1 {
|
||||
filters := strings.Join(attributes.Filters, " ")
|
||||
request.Filter = &filters
|
||||
}
|
||||
|
||||
return request
|
||||
@@ -131,8 +133,9 @@ func buildListInstanceGroupsRequest(attributes *SetAttributes, catalog *CatalogA
|
||||
Zone: catalog.Zone,
|
||||
}
|
||||
|
||||
if len(attributes.Filter) > 1 {
|
||||
request.Filter = &attributes.Filter
|
||||
if len(attributes.Filters) > 1 {
|
||||
filters := strings.Join(attributes.Filters, " ")
|
||||
request.Filter = &filters
|
||||
}
|
||||
|
||||
return request
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
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"},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ package host
|
||||
|
||||
const (
|
||||
// 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 = "instance_group"
|
||||
|
||||
@@ -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.
|
||||
@@ -6,6 +6,8 @@ package host
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||
@@ -36,6 +38,13 @@ var (
|
||||
_ 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.
|
||||
func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
||||
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)
|
||||
}
|
||||
|
||||
if st := dryRunValidation(ctx, catalogState, p.testGCPClientOpts...); st != nil {
|
||||
if st := dryRunValidation(ctx, catalogState, nil, p.testGCPClientOpts...); st != nil {
|
||||
return nil, st.Err()
|
||||
}
|
||||
|
||||
@@ -158,7 +167,7 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
if err != nil {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
if st := dryRunValidation(ctx, catalogState, p.testGCPClientOpts...); st != nil {
|
||||
if st := dryRunValidation(ctx, catalogState, nil, p.testGCPClientOpts...); st != nil {
|
||||
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.
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -485,14 +548,14 @@ func validateSet(s *hostsets.HostSet) error {
|
||||
if instanceGroupSet && filterSet {
|
||||
badFields["attributes"] = "must set instance group or filter, cannot set both"
|
||||
} else if instanceGroupSet && len(attrs.InstanceGroup) == 0 {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstInstanceGroup)] = "must not be empty."
|
||||
} else if filterSet && len(attrs.Filter) == 0 {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty."
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstInstanceGroup)] = "must not be empty"
|
||||
} else if filterSet && len(attrs.Filters) == 0 {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty"
|
||||
}
|
||||
|
||||
for f := range attrMap {
|
||||
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(
|
||||
ctx context.Context,
|
||||
state *gcpCatalogPersistedState,
|
||||
listFilters []string,
|
||||
clientOptions ...option.ClientOption,
|
||||
) *status.Status {
|
||||
if state == nil {
|
||||
@@ -518,9 +582,15 @@ func dryRunValidation(
|
||||
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{
|
||||
Project: state.CredentialsConfig.ProjectId,
|
||||
Zone: state.CredentialsConfig.Zone,
|
||||
Filter: &filters,
|
||||
})
|
||||
|
||||
_, err = it.Next()
|
||||
@@ -530,3 +600,62 @@ func dryRunValidation(
|
||||
|
||||
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
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package host
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -949,17 +950,41 @@ func TestDeleteCatalog(t *testing.T) {
|
||||
|
||||
func TestCreateSet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
p := &HostPlugin{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.OnCreateSetRequest
|
||||
expectedErr string
|
||||
name string
|
||||
req *pb.OnCreateSetRequest
|
||||
catalogOpts []gcpCatalogPersistedStateOption
|
||||
gcpOptions []option.ClientOption
|
||||
expectedErr string
|
||||
expectedErrCode codes.Code
|
||||
}{
|
||||
{
|
||||
name: "nil set",
|
||||
req: &pb.OnCreateSetRequest{},
|
||||
expectedErr: "set is nil",
|
||||
name: "nil host catalog",
|
||||
req: &pb.OnCreateSetRequest{},
|
||||
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",
|
||||
@@ -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{
|
||||
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{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstListInstancesFilter: structpb.NewStringValue("status=RUNNING"),
|
||||
ConstInstanceGroup: structpb.NewStringValue("test"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"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{
|
||||
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{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
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{
|
||||
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{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
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{
|
||||
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{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
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{
|
||||
Set: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
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) {
|
||||
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 != "" {
|
||||
require.Contains(err.Error(), tc.expectedErr)
|
||||
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1106,8 +1342,14 @@ func TestUpdateSet(t *testing.T) {
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstListInstancesFilter: structpb.NewStringValue("status=RUNNING"),
|
||||
ConstInstanceGroup: structpb.NewStringValue("test"),
|
||||
ConstListInstancesFilter: structpb.NewListValue(
|
||||
&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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
req: &pb.OnUpdateSetRequest{
|
||||
|
||||
Reference in new issue
Block a user