Files
threshold-plugin-gcp/plugin/service/host/plugin_test.go
T

2418 lines
73 KiB
Go
Raw Normal View History

2024-04-02 15:47:49 -04:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2024-09-16 14:28:54 -04:00
package host
2024-04-02 15:47:49 -04:00
import (
"context"
2024-10-28 12:50:14 -04:00
"errors"
2024-10-28 11:00:14 -04:00
"fmt"
2024-10-18 10:35:05 -04:00
"net/http"
2024-04-02 15:47:49 -04:00
"testing"
2024-10-28 10:48:26 -04:00
"time"
2024-04-02 15:47:49 -04:00
2024-10-21 10:47:06 -04:00
"cloud.google.com/go/compute/apiv1/computepb"
"cloud.google.com/go/iam/admin/apiv1/adminpb"
"cloud.google.com/go/iam/apiv1/iampb"
"cloud.google.com/go/resourcemanager/apiv3/resourcemanagerpb"
"github.com/google/go-cmp/cmp"
2024-09-16 12:20:08 -04:00
cred "github.com/hashicorp/boundary-plugin-gcp/internal/credential"
2024-04-02 15:47:49 -04:00
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs"
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
"github.com/stretchr/testify/require"
2024-10-21 10:47:06 -04:00
"google.golang.org/api/option"
"google.golang.org/grpc"
2024-10-28 11:00:14 -04:00
"google.golang.org/grpc/codes"
2024-10-21 10:47:06 -04:00
"google.golang.org/grpc/credentials/insecure"
2024-10-28 11:00:14 -04:00
"google.golang.org/grpc/status"
2024-10-21 10:47:06 -04:00
"google.golang.org/protobuf/testing/protocmp"
2024-04-02 15:47:49 -04:00
"google.golang.org/protobuf/types/known/structpb"
)
func wrapMap(t *testing.T, in map[string]interface{}) *structpb.Struct {
t.Helper()
out, err := structpb.NewStruct(in)
require.NoError(t, err)
return out
}
func TestListHosts(t *testing.T) {
ctx := context.Background()
cases := []struct {
2024-10-28 12:50:14 -04:00
name string
req *pb.ListHostsRequest
catalogOpts []gcpCatalogPersistedStateOption
gcpOptions []option.ClientOption
expected []*pb.ListHostsResponseHost
expectedErr string
expectedErrCode codes.Code
2024-04-02 15:47:49 -04:00
}{
{
2024-10-28 12:50:14 -04:00
name: "nil catalog",
req: &pb.ListHostsRequest{},
expectedErr: "catalog is nil",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil catalog attributes",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
},
},
expectedErr: "catalog missing attributes",
expectedErrCode: codes.InvalidArgument,
},
{
name: "zone not defined",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErr: "attributes.zone: missing required value \"zone\"",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "project_id not defined",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: wrapMap(t, map[string]interface{}{
2024-10-28 12:50:14 -04:00
cred.ConstZone: "us-central1-c",
2024-04-02 15:47:49 -04:00
}),
},
},
},
2024-10-28 12:50:14 -04:00
expectedErr: "attributes.project_id: missing required value \"project_id\"",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "persisted state setup error",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
2024-10-28 12:50:14 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
cred.ConstZone: structpb.NewStringValue("us-central1-c"),
},
},
},
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
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"),
2024-04-02 15:47:49 -04:00
},
},
},
},
2024-10-28 12:50:14 -04:00
catalogOpts: []gcpCatalogPersistedStateOption{
func(s *gcpCatalogPersistedState) error {
return errors.New("error loading persisted state")
2024-04-02 15:47:49 -04:00
},
},
2024-10-28 12:50:14 -04:00
expectedErr: "error loading persisted state",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "set missing id",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
2024-10-28 12:50:14 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
cred.ConstZone: structpb.NewStringValue("us-central1-c"),
},
2024-04-02 15:47:49 -04:00
},
},
},
2024-10-28 12:50:14 -04:00
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"),
},
},
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
Sets: []*hostsets.HostSet{{}},
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
expectedErr: "set missing id",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "set missing attributes",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
2024-10-28 12:50:14 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
cred.ConstZone: structpb.NewStringValue("us-central1-c"),
},
2024-04-02 15:47:49 -04:00
},
},
},
2024-10-28 12:50:14 -04:00
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"),
},
},
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
Sets: []*hostsets.HostSet{
{
Id: "foobar",
},
2024-04-02 15:47:49 -04:00
},
},
2024-10-28 12:50:14 -04:00
expectedErr: "set foobar missing attributes",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "set attribute load error",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
2024-10-28 12:50:14 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
cred.ConstZone: structpb.NewStringValue("us-central1-c"),
},
},
},
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
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"),
2024-04-02 15:47:49 -04:00
},
},
2024-10-28 12:50:14 -04:00
},
Sets: []*hostsets.HostSet{
2024-04-02 15:47:49 -04:00
{
2024-10-28 12:50:14 -04:00
Id: "foobar",
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
2024-10-28 12:50:14 -04:00
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
2024-04-02 15:47:49 -04:00
},
},
},
},
2024-10-28 12:50:14 -04:00
expectedErr: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "client load error",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
2024-10-28 12:50:14 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
cred.ConstZone: structpb.NewStringValue("us-central1-c"),
},
},
},
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
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"),
2024-04-02 15:47:49 -04:00
},
},
2024-10-28 12:50:14 -04:00
},
Sets: []*hostsets.HostSet{
2024-04-02 15:47:49 -04:00
{
2024-10-28 12:50:14 -04:00
Id: "foobar",
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
2024-10-28 12:50:14 -04:00
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
2024-04-02 15:47:49 -04:00
},
},
},
},
2024-10-28 12:50:14 -04:00
gcpOptions: []option.ClientOption{
option.WithHTTPClient(&http.Client{}),
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
expectedErr: "error getting instances client",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "List instances error",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
2024-10-28 12:50:14 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
cred.ConstZone: structpb.NewStringValue("us-central1-c"),
},
},
},
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
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"),
2024-04-02 15:47:49 -04:00
},
},
2024-10-28 12:50:14 -04:00
},
Sets: []*hostsets.HostSet{
2024-04-02 15:47:49 -04:00
{
2024-10-28 12:50:14 -04:00
Id: "foobar",
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
2024-10-28 12:50:14 -04:00
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
2024-04-02 15:47:49 -04:00
},
},
},
},
2024-10-28 12:50:14 -04:00
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesError(fmt.Errorf("error running DescribeInstances for host set id \"foobar\"")))),
2024-04-02 15:47:49 -04:00
},
2024-10-28 12:50:14 -04:00
expectedErr: "error running list instances for host set id \"foobar\"",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "instanceToHost error",
2024-04-02 15:47:49 -04:00
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
2024-10-28 12:50:14 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
cred.ConstZone: structpb.NewStringValue("us-central1-c"),
},
2024-04-02 15:47:49 -04:00
},
},
},
2024-10-28 12:50:14 -04:00
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"),
},
},
},
Sets: []*hostsets.HostSet{
{
Id: "foobar",
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"),
},
},
),
},
},
},
2024-04-02 15:47:49 -04:00
},
},
},
2024-10-28 12:50:14 -04:00
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
Name: pointer("boundary-0"),
},
},
}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedErr: "error processing host results for host set id \"foobar\": response integrity error: missing instance id",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "get all three instances",
req: &pb.ListHostsRequest{
2024-04-02 15:47:49 -04:00
Catalog: &hostcatalogs.HostCatalog{
2024-10-21 10:47:06 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
2024-10-28 12:50:14 -04:00
Attributes: wrapMap(t, map[string]interface{}{
cred.ConstZone: "us-central1-c",
cred.ConstProjectId: "test-project",
}),
2024-10-21 10:47:06 -04:00
},
2024-10-28 12:50:14 -04:00
},
Sets: []*hostsets.HostSet{
{
Id: "get-all-instances",
Attrs: &hostsets.HostSet_Attributes{
Attributes: wrapMap(t, map[string]interface{}{}),
2024-10-21 10:47:06 -04:00
},
},
2024-04-02 15:47:49 -04:00
},
},
2024-10-21 10:47:06 -04:00
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
Name: pointer("boundary-0"),
2024-10-28 12:50:14 -04:00
Id: pointer(uint64(1)),
2024-10-21 10:47:06 -04:00
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
2024-10-28 12:50:14 -04:00
NatIP: pointer("101.1.1.1"),
ExternalIpv6: pointer("2001:db8::1"),
},
},
Ipv6AccessConfigs: []*computepb.AccessConfig{
{
2024-10-21 10:47:06 -04:00
ExternalIpv6: pointer("2001:db8::1"),
},
},
},
},
},
2024-10-28 12:50:14 -04:00
{
Name: pointer("boundary-1"),
Id: pointer(uint64(2)),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("102.1.1.1"),
},
},
Ipv6AccessConfigs: []*computepb.AccessConfig{
{
ExternalIpv6: pointer("2001:db8::2"),
},
},
},
},
},
{
Name: pointer("boundary-2"),
Id: pointer(uint64(3)),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("103.1.1.1"),
ExternalIpv6: pointer("2001:db8::3"),
},
},
Ipv6AccessConfigs: []*computepb.AccessConfig{
{
ExternalIpv6: pointer("2001:db8::3"),
},
},
},
},
},
2024-10-21 10:47:06 -04:00
},
}),
testMockInstancesWithListInstancesError(nil),
)),
},
2024-10-28 12:50:14 -04:00
expected: []*pb.ListHostsResponseHost{
{
SetIds: []string{"get-all-instances"},
ExternalName: "boundary-0",
ExternalId: "1",
IpAddresses: []string{"101.1.1.1", "2001:db8::1"},
},
{
SetIds: []string{"get-all-instances"},
ExternalName: "boundary-1",
ExternalId: "2",
IpAddresses: []string{"102.1.1.1", "2001:db8::2"},
},
{
SetIds: []string{"get-all-instances"},
ExternalName: "boundary-2",
ExternalId: "3",
IpAddresses: []string{"103.1.1.1", "2001:db8::3"},
2024-10-21 10:47:06 -04:00
},
},
},
{
2024-10-28 12:50:14 -04:00
name: "get one instance by name",
req: &pb.ListHostsRequest{
2024-10-21 10:47:06 -04:00
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
2024-10-28 12:50:14 -04:00
Attributes: wrapMap(t, map[string]interface{}{
cred.ConstZone: "us-central1-c",
cred.ConstProjectId: "test-project",
}),
2024-10-21 10:47:06 -04:00
},
2024-10-28 12:50:14 -04:00
},
Sets: []*hostsets.HostSet{
{
Id: "get-one-instance-by-name",
Attrs: &hostsets.HostSet_Attributes{
Attributes: wrapMap(t, map[string]interface{}{
ConstListInstancesFilter: "name = boundary-1",
}),
2024-10-21 10:47:06 -04:00
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
2024-10-28 12:50:14 -04:00
Name: pointer("boundary-1"),
Id: pointer(uint64(1)),
2024-10-21 10:47:06 -04:00
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("102.1.1.1"),
ExternalIpv6: pointer("2001:db8::1"),
},
},
2024-10-28 12:50:14 -04:00
Ipv6AccessConfigs: []*computepb.AccessConfig{
{
ExternalIpv6: pointer("2001:db8::1"),
},
},
2024-10-21 10:47:06 -04:00
},
},
},
},
}),
testMockInstancesWithListInstancesError(nil),
)),
},
2024-10-28 12:50:14 -04:00
expected: []*pb.ListHostsResponseHost{
{
SetIds: []string{"get-one-instance-by-name"},
ExternalName: "boundary-1",
ExternalId: "1",
IpAddresses: []string{
"102.1.1.1",
"2001:db8::1",
2024-10-21 10:47:06 -04:00
},
},
},
},
{
2024-10-28 12:50:14 -04:00
name: "get two specific instances with two host sets",
req: &pb.ListHostsRequest{
2024-10-21 10:47:06 -04:00
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
2024-10-28 12:50:14 -04:00
Attributes: wrapMap(t, map[string]interface{}{
cred.ConstZone: "us-central1-c",
cred.ConstProjectId: "test-project",
}),
},
},
Sets: []*hostsets.HostSet{
{
Id: "get-one-instance-by-name-0",
Attrs: &hostsets.HostSet_Attributes{
Attributes: wrapMap(t, map[string]interface{}{
ConstListInstancesFilter: "name = boundary-0",
}),
2024-10-21 10:47:06 -04:00
},
},
2024-10-28 12:50:14 -04:00
{
Id: "get-one-instance-by-name-2",
Attrs: &hostsets.HostSet_Attributes{
Attributes: wrapMap(t, map[string]interface{}{
ConstListInstancesFilter: "name = boundary-2",
}),
2024-10-21 10:47:06 -04:00
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
2024-10-28 12:50:14 -04:00
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
Name: pointer("boundary-1"),
Id: pointer(uint64(1)),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("102.1.1.1"),
},
},
},
},
},
{
Name: pointer("boundary-2"),
Id: pointer(uint64(2)),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("103.1.1.1"),
},
},
Ipv6AccessConfigs: []*computepb.AccessConfig{
{
ExternalIpv6: pointer("2001:db8::1"),
},
},
},
},
},
},
}),
2024-10-21 10:47:06 -04:00
testMockInstancesWithListInstancesError(nil),
)),
},
2024-10-28 12:50:14 -04:00
expected: []*pb.ListHostsResponseHost{
{
SetIds: []string{
"get-one-instance-by-name-0",
"get-one-instance-by-name-2",
},
ExternalId: "1",
ExternalName: "boundary-1",
IpAddresses: []string{
"102.1.1.1",
},
},
{
SetIds: []string{
"get-one-instance-by-name-0",
"get-one-instance-by-name-2",
},
ExternalId: "2",
ExternalName: "boundary-2",
IpAddresses: []string{
"103.1.1.1",
"2001:db8::1",
},
},
},
},
{
name: "invalid filter",
req: &pb.ListHostsRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: wrapMap(t, map[string]interface{}{
cred.ConstZone: "us-central1-c",
cred.ConstProjectId: "test-project",
}),
},
},
Sets: []*hostsets.HostSet{
{
Id: "invalid-filter",
Attrs: &hostsets.HostSet_Attributes{
Attributes: wrapMap(t, map[string]interface{}{
ConstListInstancesFilter: "not-a-filter",
}),
},
},
},
},
2024-11-06 12:43:06 -05:00
expectedErr: "error building filters: invalid filter \"not-a-filter\"",
2024-10-28 12:50:14 -04:00
expectedErrCode: codes.InvalidArgument,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
p := &HostPlugin{
testGCPClientOpts: tc.gcpOptions,
testCatalogStateOpts: tc.catalogOpts,
}
actual, err := p.ListHosts(ctx, tc.req)
if tc.expectedErr != "" {
require.Contains(err.Error(), tc.expectedErr)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
return
}
require.NoError(err)
require.Equal(len(tc.expected), len(actual.GetHosts()))
require.Equal(tc.expected, actual.GetHosts())
})
}
}
func TestCreateCatalog(t *testing.T) {
ctx := context.Background()
cases := []struct {
name string
req *pb.OnCreateCatalogRequest
expected *pb.HostCatalogPersisted
catalogOpts []gcpCatalogPersistedStateOption
expectedErr string
expectedRsp *pb.OnCreateCatalogResponse
}{
{
name: "nil catalog",
req: &pb.OnCreateCatalogRequest{},
expectedErr: "catalog is nil",
},
{
name: "nil attributes",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{},
},
expectedErr: "attributes are required",
},
{
name: "error reading attributes",
req: &pb.OnCreateCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErr: "attributes.project_id: missing required value \"project_id\"",
},
{
name: "using static credentials",
req: &pb.OnCreateCatalogRequest{
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"),
cred.ConstClientEmail: structpb.NewStringValue("test-client-email@email.com"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
Name: pointer("boundary-0"),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("102.1.1.1"),
ExternalIpv6: pointer("2001:db8::1"),
},
},
},
},
},
},
}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedRsp: &pb.OnCreateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
},
},
},
},
},
{
name: "using rotated static credentials",
req: &pb.OnCreateCatalogRequest{
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"),
cred.ConstClientEmail: structpb.NewStringValue("test-client-email@email.com"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
Name: pointer("boundary-0"),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("102.1.1.1"),
ExternalIpv6: pointer("2001:db8::1"),
},
},
},
},
},
},
}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedRsp: &pb.OnCreateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKey: structpb.NewStringValue("updated-private-key"),
cred.ConstPrivateKeyId: structpb.NewStringValue("updated-private-key-id"),
},
},
},
},
},
{
name: "using impersonated credentials",
req: &pb.OnCreateCatalogRequest{
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"),
cred.ConstClientEmail: structpb.NewStringValue("test-client-email@email.com"),
cred.ConstTargetServiceAccountID: structpb.NewStringValue("test-target-service-account-id"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedRsp: &pb.OnCreateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKey: structpb.NewStringValue("test-private-key"),
2024-10-21 10:47:06 -04:00
cred.ConstPrivateKeyId: structpb.NewStringValue("test-private-key-id"),
},
},
},
2024-04-02 15:47:49 -04:00
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
2024-10-21 10:47:06 -04:00
testIAMAdminServer := cred.NewTestIAMAdminServer(nil, nil)
testResourceServer := cred.NewTestResourceServer(&iampb.TestIamPermissionsResponse{
Permissions: []string{
cred.ComputeInstancesListPermission,
cred.IAMServiceAccountKeysCreatePermission,
cred.IAMServiceAccountKeysDeletePermission,
},
}, nil)
gsrv := cred.NewGRPCServer()
adminpb.RegisterIAMServer(gsrv.Server, testIAMAdminServer)
resourcemanagerpb.RegisterProjectsServer(gsrv.Server, testResourceServer)
addr, err := gsrv.Start()
require.NoError(err)
p := &HostPlugin{
testGCPClientOpts: []option.ClientOption{
option.WithEndpoint(addr),
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
option.WithTokenSource(nil),
},
testCatalogStateOpts: tc.catalogOpts,
}
2024-04-02 15:47:49 -04:00
actual, err := p.OnCreateCatalog(ctx, tc.req)
if tc.expectedErr != "" {
require.Contains(err.Error(), tc.expectedErr)
return
}
require.NoError(err)
2024-10-21 10:47:06 -04:00
delete(actual.GetPersisted().GetSecrets().GetFields(), cred.ConstCredsLastRotatedTime)
require.Empty(cmp.Diff(tc.expectedRsp, actual, protocmp.Transform()))
2024-04-02 15:47:49 -04:00
})
}
}
func TestUpdateCatalog(t *testing.T) {
ctx := context.Background()
cases := []struct {
name string
2024-10-28 10:48:26 -04:00
req *pb.OnUpdateCatalogRequest
catalogOpts []gcpCatalogPersistedStateOption
expectedRsp *pb.OnUpdateCatalogResponse
2024-04-02 15:47:49 -04:00
expectedErr string
}{
{
2024-10-28 10:48:26 -04:00
name: "nil current catalog",
req: &pb.OnUpdateCatalogRequest{},
expectedErr: "current catalog is required",
},
{
name: "nil new catalog",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{},
},
expectedErr: "new catalog is required",
2024-04-02 15:47:49 -04:00
},
{
name: "nil attributes",
2024-10-28 10:48:26 -04:00
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{},
NewCatalog: &hostcatalogs.HostCatalog{},
2024-04-02 15:47:49 -04:00
},
expectedErr: "attributes are required",
},
{
name: "error reading attributes",
2024-10-28 10:48:26 -04:00
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
NewCatalog: &hostcatalogs.HostCatalog{
2024-04-02 15:47:49 -04:00
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
2024-10-28 10:48:26 -04:00
expectedErr: "attributes.project_id: missing required value \"project_id\"",
2024-04-02 15:47:49 -04:00
},
{
2024-10-28 10:48:26 -04:00
name: "staticCredentialToStaticRotated",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstClientEmail: structpb.NewStringValue("test@example.com"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
NewCatalog: &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"),
cred.ConstClientEmail: structpb.NewStringValue("test@example.com"),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("new-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("new-private-key"),
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("persisted-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("persisted-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Time{}.Format(time.RFC3339Nano)),
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("updated-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("updated-private-key"),
},
},
},
},
},
{
name: "staticRotatedCredentialToStatic",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstClientEmail: structpb.NewStringValue("test@example.com"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
NewCatalog: &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"),
cred.ConstClientEmail: structpb.NewStringValue("test@example.com"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("not-rotated-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("not-rotated-private-key"),
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("persisted-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("persisted-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Now().Format(time.RFC3339Nano)),
},
},
},
},
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("not-rotated-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("not-rotated-private-key"),
},
},
},
},
},
{
name: "staticCredentialToImpersonateCredential",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstClientEmail: structpb.NewStringValue("test@example.com"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
},
NewCatalog: &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"),
cred.ConstClientEmail: structpb.NewStringValue("test@example.com"),
cred.ConstTargetServiceAccountID: structpb.NewStringValue("test-target-service-account-id"),
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
},
},
},
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("new-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("new-private-key"),
},
},
},
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("persisted-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("persisted-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Time{}.Format(time.RFC3339Nano)),
},
},
2024-04-02 15:47:49 -04:00
},
},
2024-10-28 10:48:26 -04:00
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil),
)),
},
expectedRsp: &pb.OnUpdateCatalogResponse{
Persisted: &pb.HostCatalogPersisted{
Secrets: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstPrivateKeyId: structpb.NewStringValue("new-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("new-private-key"),
},
},
},
2024-04-02 15:47:49 -04:00
},
},
2024-10-28 10:48:26 -04:00
{
name: "staticRotatedCredentialToAppDefaultCredential",
req: &pb.OnUpdateCatalogRequest{
CurrentCatalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstClientEmail: structpb.NewStringValue("test@example.com"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
},
},
},
},
NewCatalog: &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("persisted-private-key-id"),
cred.ConstPrivateKey: structpb.NewStringValue("persisted-private-key"),
cred.ConstCredsLastRotatedTime: structpb.NewStringValue(time.Time{}.Format(time.RFC3339Nano)),
},
},
},
},
expectedErr: "cannot rotate credentials for non-rotatable credentials",
},
2024-04-02 15:47:49 -04:00
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
2024-10-28 10:48:26 -04:00
testIAMAdminServer := cred.NewTestIAMAdminServer(nil, nil)
testResourceServer := cred.NewTestResourceServer(&iampb.TestIamPermissionsResponse{
Permissions: []string{
cred.ComputeInstancesListPermission,
cred.IAMServiceAccountKeysCreatePermission,
cred.IAMServiceAccountKeysDeletePermission,
},
}, nil)
gsrv := cred.NewGRPCServer()
adminpb.RegisterIAMServer(gsrv.Server, testIAMAdminServer)
resourcemanagerpb.RegisterProjectsServer(gsrv.Server, testResourceServer)
addr, err := gsrv.Start()
require.NoError(err)
p := &HostPlugin{
testGCPClientOpts: []option.ClientOption{
option.WithEndpoint(addr),
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
option.WithTokenSource(nil),
},
testCatalogStateOpts: tc.catalogOpts,
}
actual, err := p.OnUpdateCatalog(ctx, tc.req)
2024-04-02 15:47:49 -04:00
if tc.expectedErr != "" {
require.Contains(err.Error(), tc.expectedErr)
return
}
require.NoError(err)
2024-10-28 10:48:26 -04:00
delete(actual.GetPersisted().GetSecrets().GetFields(), cred.ConstCredsLastRotatedTime)
require.Empty(cmp.Diff(tc.expectedRsp, actual, protocmp.Transform()))
2024-04-02 15:47:49 -04:00
})
}
}
2024-10-28 11:00:14 -04:00
func TestDeleteCatalog(t *testing.T) {
cases := []struct {
name string
req *pb.OnDeleteCatalogRequest
catalogOpts []gcpCatalogPersistedStateOption
deleteCredError error
expectedRsp *pb.OnDeleteCatalogResponse
expectedErr string
expectedErrCode codes.Code
}{
{
2024-10-28 12:50:14 -04:00
name: "persisted state setup error",
req: &pb.OnDeleteCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
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: "delete error",
req: &pb.OnDeleteCatalogRequest{
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
cred.ConstClientEmail: structpb.NewStringValue("test@test.com"),
cred.ConstProjectId: structpb.NewStringValue("test-project-id"),
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"),
},
},
},
},
deleteCredError: fmt.Errorf("failed to delete access key"),
expectedErr: "failed to delete access key",
expectedErrCode: codes.Unknown,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
testIAMAdminServer := cred.NewTestIAMAdminServer(nil, tc.deleteCredError)
gsrv := cred.NewGRPCServer()
adminpb.RegisterIAMServer(gsrv.Server, testIAMAdminServer)
addr, err := gsrv.Start()
require.NoError(err)
p := &HostPlugin{
testGCPClientOpts: []option.ClientOption{
option.WithEndpoint(addr),
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
option.WithTokenSource(nil),
},
testCatalogStateOpts: tc.catalogOpts,
}
_, err = p.OnDeleteCatalog(context.Background(), tc.req)
require.Error(err)
require.Contains(err.Error(), tc.expectedErr)
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
})
}
}
func TestCreateSet(t *testing.T) {
ctx := context.Background()
cases := []struct {
name string
req *pb.OnCreateSetRequest
catalogOpts []gcpCatalogPersistedStateOption
gcpOptions []option.ClientOption
expectedErr string
expectedErrCode codes.Code
}{
{
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",
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"),
},
},
},
},
},
expectedErr: "set is required",
expectedErrCode: codes.InvalidArgument,
},
{
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{
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
},
},
},
},
},
expectedErr: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
expectedErrCode: codes.InvalidArgument,
},
{
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.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("invalid"),
},
},
),
},
},
},
},
},
expectedErr: "error building filters",
expectedErrCode: codes.InvalidArgument,
},
{
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{
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
},
},
},
},
},
gcpOptions: []option.ClientOption{
option.WithHTTPClient(&http.Client{}),
},
expectedErr: "error getting instances client",
expectedErrCode: codes.InvalidArgument,
},
{
name: "List instances error",
req: &pb.OnCreateSetRequest{
2024-10-28 11:00:14 -04:00
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-10-28 12:50:14 -04:00
cred.ConstProjectId: structpb.NewStringValue("test-project"),
2024-10-28 11:00:14 -04:00
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"),
},
},
},
2024-10-28 12:50:14 -04: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"),
},
},
),
},
},
},
},
2024-10-28 11:00:14 -04:00
},
catalogOpts: []gcpCatalogPersistedStateOption{
2024-10-28 12:50:14 -04:00
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesError(fmt.Errorf("failed to list instances")),
)),
2024-10-28 11:00:14 -04:00
},
2024-10-28 12:50:14 -04:00
expectedErr: "gcp list instances failed",
expectedErrCode: codes.FailedPrecondition,
2024-10-28 11:00:14 -04:00
},
{
2024-10-28 12:50:14 -04:00
name: "success",
req: &pb.OnCreateSetRequest{
2024-10-28 11:00:14 -04:00
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-10-28 12:50:14 -04:00
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
2024-10-28 11:00:14 -04:00
},
},
},
},
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"),
},
},
},
2024-10-28 12:50:14 -04: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)),
),
2024-10-28 11:00:14 -04:00
},
},
}
for _, tc := range cases {
2024-10-28 12:50:14 -04:00
tc := tc
2024-10-28 11:00:14 -04:00
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
2024-10-28 12:50:14 -04:00
testIAMAdminServer := cred.NewTestIAMAdminServer(nil, nil)
2024-10-28 11:00:14 -04:00
gsrv := cred.NewGRPCServer()
adminpb.RegisterIAMServer(gsrv.Server, testIAMAdminServer)
addr, err := gsrv.Start()
require.NoError(err)
2024-10-28 12:50:14 -04:00
clientOptions := []option.ClientOption{
option.WithEndpoint(addr),
option.WithoutAuthentication(),
option.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
option.WithTokenSource(nil),
}
clientOptions = append(clientOptions, tc.gcpOptions...)
2024-10-28 11:00:14 -04:00
p := &HostPlugin{
2024-10-28 12:50:14 -04:00
testGCPClientOpts: clientOptions,
2024-10-28 11:00:14 -04:00
testCatalogStateOpts: tc.catalogOpts,
}
2024-10-28 12:50:14 -04:00
_, 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
}
2024-10-28 11:00:14 -04:00
2024-10-28 12:50:14 -04:00
require.NoError(err)
2024-10-28 11:00:14 -04:00
})
}
}
2024-10-28 12:50:14 -04:00
func TestUpdateSet(t *testing.T) {
2024-04-02 15:47:49 -04:00
ctx := context.Background()
cases := []struct {
2024-10-18 10:35:05 -04:00
name string
2024-10-28 12:50:14 -04:00
req *pb.OnUpdateSetRequest
2024-10-18 10:35:05 -04:00
catalogOpts []gcpCatalogPersistedStateOption
gcpOptions []option.ClientOption
expectedErr string
expectedErrCode codes.Code
2024-04-02 15:47:49 -04:00
}{
{
2024-10-18 10:35:05 -04:00
name: "nil host catalog",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{},
2024-10-18 10:35:05 -04:00
expectedErr: "catalog is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "nil host catalog attributes",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
Catalog: &hostcatalogs.HostCatalog{},
},
expectedErr: "catalog attributes are required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error reading catalog attributes",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
Catalog: &hostcatalogs.HostCatalog{
Secrets: new(structpb.Struct),
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: new(structpb.Struct),
},
},
},
expectedErr: "missing required value \"project_id\"",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
name: "nil set",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-04-02 15:47:49 -04:00
Catalog: &hostcatalogs.HostCatalog{
Attrs: &hostcatalogs.HostCatalog_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-09-17 10:50:10 -04:00
cred.ConstProjectId: structpb.NewStringValue("test-project"),
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
2024-04-02 15:47:49 -04:00
},
},
},
},
},
2024-10-18 10:35:05 -04:00
expectedErr: "set is required",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-18 10:35:05 -04:00
name: "persisted state setup error",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
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",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
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"),
},
},
},
2024-10-28 12:50:14 -04:00
NewSet: &hostsets.HostSet{},
2024-10-18 10:35:05 -04:00
},
expectedErr: "set attributes are required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "set attribute load error",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
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"),
},
},
},
2024-10-28 12:50:14 -04:00
NewSet: &hostsets.HostSet{
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-10-18 10:35:05 -04:00
"foo": structpb.NewBoolValue(true),
"bar": structpb.NewBoolValue(true),
2024-04-02 15:47:49 -04:00
},
},
},
},
},
2024-10-18 10:35:05 -04:00
expectedErr: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-18 10:35:05 -04:00
name: "invalid filter",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
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"),
},
},
},
},
2024-10-28 12:50:14 -04:00
NewSet: &hostsets.HostSet{
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-10-18 10:35:05 -04:00
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("invalid"),
},
},
),
2024-04-02 15:47:49 -04:00
},
},
},
},
},
2024-10-18 10:35:05 -04:00
expectedErr: "error building filters",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-18 10:35:05 -04:00
name: "client load error",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
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"),
},
},
},
2024-10-28 12:50:14 -04:00
NewSet: &hostsets.HostSet{
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-10-18 10:35:05 -04:00
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
2024-04-02 15:47:49 -04:00
},
},
},
},
},
2024-10-18 10:35:05 -04:00
gcpOptions: []option.ClientOption{
option.WithHTTPClient(&http.Client{}),
},
expectedErr: "error getting instances client",
expectedErrCode: codes.InvalidArgument,
2024-04-02 15:47:49 -04:00
},
{
2024-10-18 10:35:05 -04:00
name: "List instances error",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
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"),
},
},
},
2024-10-28 12:50:14 -04:00
NewSet: &hostsets.HostSet{
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-10-18 10:35:05 -04:00
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
2024-04-02 15:47:49 -04:00
},
},
},
},
},
2024-10-18 10:35:05 -04:00
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesError(fmt.Errorf("failed to list instances")),
)),
},
expectedErr: "gcp list instances failed",
expectedErrCode: codes.FailedPrecondition,
2024-04-02 15:47:49 -04:00
},
{
2024-10-18 10:35:05 -04:00
name: "success",
2024-10-28 12:50:14 -04:00
req: &pb.OnUpdateSetRequest{
2024-10-18 10:35:05 -04:00
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"),
},
},
},
2024-10-28 12:50:14 -04:00
NewSet: &hostsets.HostSet{
2024-04-02 15:47:49 -04:00
Attrs: &hostsets.HostSet_Attributes{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
2024-10-18 10:35:05 -04:00
ConstListInstancesFilter: structpb.NewListValue(
&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("tag-key=foo"),
},
},
),
2024-04-02 15:47:49 -04:00
},
},
},
},
},
2024-10-18 10:35:05 -04:00
catalogOpts: []gcpCatalogPersistedStateOption{
withTestInstancesAPIFunc(newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
testMockInstancesWithListInstancesError(nil)),
),
},
2024-04-02 15:47:49 -04:00
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
2024-10-18 10:35:05 -04:00
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,
}
2024-10-28 12:50:14 -04:00
_, err = p.OnUpdateSet(ctx, tc.req)
2024-04-02 15:47:49 -04:00
if tc.expectedErr != "" {
require.Contains(err.Error(), tc.expectedErr)
2024-10-18 10:35:05 -04:00
require.Equal(status.Code(err).String(), tc.expectedErrCode.String())
2024-04-02 15:47:49 -04:00
return
}
require.NoError(err)
})
}
}
2024-10-28 11:02:12 -04:00
func TestNormalizeSetData(t *testing.T) {
ctx := context.Background()
p := &HostPlugin{}
cases := []struct {
name string
req *pb.NormalizeSetDataRequest
expectedRsp *pb.NormalizeSetDataResponse
expectedErrContains string
}{
{
name: "nil attributes",
req: &pb.NormalizeSetDataRequest{},
expectedRsp: &pb.NormalizeSetDataResponse{
Attributes: nil,
},
},
{
name: "nil filters",
req: &pb.NormalizeSetDataRequest{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{},
},
},
expectedRsp: &pb.NormalizeSetDataResponse{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{},
},
},
},
{
name: "filters not a string",
req: &pb.NormalizeSetDataRequest{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("status = RUNNING"),
},
}),
},
},
},
expectedRsp: &pb.NormalizeSetDataResponse{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("status = RUNNING"),
},
}),
},
},
},
},
{
name: "filters is a string",
req: &pb.NormalizeSetDataRequest{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewStringValue("status = RUNNING"),
},
},
},
expectedRsp: &pb.NormalizeSetDataResponse{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("status = RUNNING"),
},
}),
},
},
},
},
{
name: "filters with multiple values",
req: &pb.NormalizeSetDataRequest{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewStringValue(`(cpuPlatform = "Intel Skylake") OR (cpuPlatform = "Intel Broadwell") AND (scheduling.automaticRestart = true)`),
},
},
},
expectedRsp: &pb.NormalizeSetDataResponse{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewListValue(&structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue(`(cpuPlatform = "Intel Skylake") OR (cpuPlatform = "Intel Broadwell") AND (scheduling.automaticRestart = true)`),
},
}),
},
},
},
},
{
name: "filters is an number",
req: &pb.NormalizeSetDataRequest{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewNumberValue(1),
},
},
},
expectedErrContains: "attribute.filters must be a string or list",
},
{
name: "filters is an boolean",
req: &pb.NormalizeSetDataRequest{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewBoolValue(true),
},
},
},
expectedErrContains: "attribute.filters must be a string or list",
},
{
name: "filters is null",
req: &pb.NormalizeSetDataRequest{
Attributes: &structpb.Struct{
Fields: map[string]*structpb.Value{
"filters": structpb.NewNullValue(),
},
},
},
expectedErrContains: "attribute.filters must be a string or list",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := p.NormalizeSetData(ctx, tc.req)
if tc.expectedErrContains != "" {
require.ErrorContains(err, tc.expectedErrContains)
return
}
require.NoError(err)
require.Empty(cmp.Diff(tc.expectedRsp, actual, protocmp.Transform()))
})
}
}
2024-11-06 12:43:06 -05:00
func TestBuildFilters(t *testing.T) {
cases := []struct {
name string
attrs *SetAttributes
expected []string
expectedErr string
}{
{
name: "no filters provided",
attrs: &SetAttributes{
Filters: []string{},
},
expected: []string{defaultStatusFilter},
},
{
name: "valid filters provided",
attrs: &SetAttributes{
Filters: []string{"name = instance-1", "zone = us-central1-a"},
},
expected: []string{"name = instance-1", "zone = us-central1-a", defaultStatusFilter},
},
{
name: "filter with invalid operator",
attrs: &SetAttributes{
Filters: []string{"name ~ instance-1"},
},
expectedErr: "invalid filter \"name ~ instance-1\"",
},
{
name: "filter with empty key",
attrs: &SetAttributes{
Filters: []string{" = instance-1"},
},
expectedErr: "invalid filter \" = instance-1\"",
},
{
name: "filter with empty value",
attrs: &SetAttributes{
Filters: []string{"name = "},
},
expectedErr: "filter \"name = \" contains an empty value",
},
{
name: "filter with status key",
attrs: &SetAttributes{
Filters: []string{"status = terminated"},
},
expected: []string{"status = terminated"},
},
{
name: "filter with multiple filters",
attrs: &SetAttributes{
Filters: []string{"status = terminated", "name = instance-1"},
},
expected: []string{"status = terminated", "name = instance-1"},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
actual, err := buildFilters(tc.attrs)
if tc.expectedErr != "" {
require.Contains(err.Error(), tc.expectedErr)
return
}
require.NoError(err)
require.Equal(tc.expected, actual)
})
}
}
func TestDryRunValidation(t *testing.T) {
ctx := context.Background()
cases := []struct {
name string
state *gcpCatalogPersistedState
listFilters []string
clientOptions []option.ClientOption
expectedErr string
expectedErrCode codes.Code
}{
{
name: "nil state",
state: nil,
expectedErr: "persisted state is required",
expectedErrCode: codes.InvalidArgument,
},
{
name: "error getting instances client",
state: &gcpCatalogPersistedState{
PersistedState: &cred.PersistedState{
CredentialsConfig: &cred.Config{
ProjectId: "test-project",
Zone: "us-central1-a",
},
},
testInstancesAPIFunc: newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(nil),
testMockInstancesWithListInstancesError(fmt.Errorf("failed to list instances")),
),
},
clientOptions: []option.ClientOption{
option.WithHTTPClient(&http.Client{}),
},
expectedErr: "gcp list instances failed",
expectedErrCode: codes.FailedPrecondition,
},
{
name: "list instances error",
state: &gcpCatalogPersistedState{
PersistedState: &cred.PersistedState{
CredentialsConfig: &cred.Config{
ProjectId: "test-project",
Zone: "us-central1-a",
},
},
testInstancesAPIFunc: newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(nil),
testMockInstancesWithListInstancesError(fmt.Errorf("failed to list instances")),
),
},
expectedErr: "gcp list instances failed",
expectedErrCode: codes.FailedPrecondition,
},
{
name: "success with filters",
state: &gcpCatalogPersistedState{
PersistedState: &cred.PersistedState{
CredentialsConfig: &cred.Config{
ProjectId: "test-project",
Zone: "us-central1-a",
},
},
testInstancesAPIFunc: newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
Name: pointer("boundary-1"),
Id: pointer(uint64(1)),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("102.1.1.1"),
ExternalIpv6: pointer("2001:db8::1"),
},
},
Ipv6AccessConfigs: []*computepb.AccessConfig{
{
ExternalIpv6: pointer("2001:db8::1"),
},
},
},
},
},
},
}),
testMockInstancesWithListInstancesError(nil),
),
},
listFilters: []string{"name = instance-1", "zone = us-central1-a"},
},
{
name: "success without filters",
state: &gcpCatalogPersistedState{
PersistedState: &cred.PersistedState{
CredentialsConfig: &cred.Config{
ProjectId: "test-project",
Zone: "us-central1-a",
},
},
testInstancesAPIFunc: newTestMockInstances(ctx,
nil,
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
Items: []*computepb.Instance{
{
Name: pointer("boundary-1"),
Id: pointer(uint64(1)),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{
NatIP: pointer("102.1.1.1"),
ExternalIpv6: pointer("2001:db8::1"),
},
},
Ipv6AccessConfigs: []*computepb.AccessConfig{
{
ExternalIpv6: pointer("2001:db8::1"),
},
},
},
},
},
},
}),
testMockInstancesWithListInstancesError(nil),
),
},
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
require := require.New(t)
st := dryRunValidation(ctx, tc.state, tc.listFilters, tc.clientOptions...)
if tc.expectedErr != "" {
require.Contains(st.Err().Error(), tc.expectedErr)
require.Equal(st.Code(), tc.expectedErrCode)
return
}
require.NoError(st.Err())
})
}
}