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 (
|
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
|
||||||
|
|||||||
@@ -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"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 (
|
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
|
||||||
|
}
|
||||||
@@ -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{
|
||||||
|
|||||||
Reference in new issue
Block a user