feat: package structure (#2)
Update structure to match other Boundary plugins which groups packages by service
This commit is contained in:
1 parent
cd73a74e1e
commit
6588e10934
8 files changed
+309
-280
No files matched your search
@@ -0,0 +1,133 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||
cred "github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||
"github.com/hashicorp/boundary-plugin-gcp/internal/errors"
|
||||
"github.com/hashicorp/boundary-plugin-gcp/internal/values"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
// CatalogAttributes defines a set of attributes for the host catalog
|
||||
type CatalogAttributes struct {
|
||||
*cred.CredentialAttributes
|
||||
}
|
||||
|
||||
func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
||||
unknownFields := values.StructFields(in)
|
||||
badFields := make(map[string]string)
|
||||
|
||||
var err error
|
||||
credAttributes, err := cred.GetCredentialAttributes(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for s := range unknownFields {
|
||||
switch s {
|
||||
// Ignore knownFields from CredentialAttributes
|
||||
case cred.ConstProject:
|
||||
continue
|
||||
case cred.ConstZone:
|
||||
continue
|
||||
default:
|
||||
badFields[fmt.Sprintf("attributes.%s", s)] = "unrecognized field"
|
||||
}
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return nil, errors.InvalidArgumentError("Invalid arguments in catalog attributes", badFields)
|
||||
}
|
||||
|
||||
return &CatalogAttributes{
|
||||
CredentialAttributes: credAttributes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetAttributes defines attributes fro the host set
|
||||
type SetAttributes struct {
|
||||
Filter string `mapstructure:"filter"`
|
||||
InstanceGroup string `mapstructure:"instance_group"`
|
||||
}
|
||||
|
||||
func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
||||
var setAttrs SetAttributes
|
||||
|
||||
badFields := make(map[string]string)
|
||||
unknownFields := values.StructFields(in)
|
||||
|
||||
if _, filter := unknownFields[ConstListInstancesFilter]; filter {
|
||||
if _, ig := unknownFields[ConstInstanceGroup]; ig {
|
||||
return nil, errors.InvalidArgumentError("Error in the attributes provided, cannot define both filter and instance group", nil)
|
||||
}
|
||||
}
|
||||
|
||||
delete(unknownFields, ConstListInstancesFilter)
|
||||
delete(unknownFields, ConstInstanceGroup)
|
||||
|
||||
for a := range unknownFields {
|
||||
badFields[fmt.Sprintf("attributes.%s", a)] = "unrecognized field"
|
||||
}
|
||||
if len(badFields) > 0 {
|
||||
return nil, errors.InvalidArgumentError("Error in the attributes provided", badFields)
|
||||
}
|
||||
|
||||
// Mapstructure complains if it expects a slice as output and sees a scalar
|
||||
// value. Rather than use WeakDecode and risk unintended consequences, I'm
|
||||
// manually making this change if necessary.
|
||||
inMap := in.AsMap()
|
||||
if filtersRaw, ok := inMap[ConstListInstancesFilter]; ok {
|
||||
switch filterVal := filtersRaw.(type) {
|
||||
case string:
|
||||
inMap[ConstListInstancesFilter] = string(filterVal)
|
||||
}
|
||||
}
|
||||
|
||||
if instanceGroupRaw, ok := inMap[ConstInstanceGroup]; ok {
|
||||
switch instanceGroupValue := instanceGroupRaw.(type) {
|
||||
case string:
|
||||
inMap[ConstInstanceGroup] = string(instanceGroupValue)
|
||||
}
|
||||
}
|
||||
|
||||
if err := mapstructure.Decode(inMap, &setAttrs); err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
|
||||
}
|
||||
|
||||
return &setAttrs, nil
|
||||
}
|
||||
|
||||
func buildListInstancesRequest(attributes *SetAttributes, catalog *CatalogAttributes) *computepb.ListInstancesRequest {
|
||||
request := &computepb.ListInstancesRequest{
|
||||
Project: catalog.Project,
|
||||
Zone: catalog.Zone,
|
||||
}
|
||||
|
||||
if len(attributes.Filter) > 1 {
|
||||
request.Filter = &attributes.Filter
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
func buildListInstanceGroupsRequest(attributes *SetAttributes, catalog *CatalogAttributes) *computepb.ListInstancesInstanceGroupsRequest {
|
||||
request := &computepb.ListInstancesInstanceGroupsRequest{
|
||||
InstanceGroup: attributes.InstanceGroup,
|
||||
Project: catalog.Project,
|
||||
Zone: catalog.Zone,
|
||||
}
|
||||
|
||||
if len(attributes.Filter) > 1 {
|
||||
request.Filter = &attributes.Filter
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
cred "github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
func TestGetCatalogAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in *structpb.Struct
|
||||
expected *CatalogAttributes
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing project and zone",
|
||||
in: &structpb.Struct{
|
||||
Fields: make(map[string]*structpb.Value),
|
||||
},
|
||||
expectedErrContains: "attributes.project: missing required value \"project\", attributes.zone: missing required value \"zone\"",
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"project": structpb.NewStringValue("test-12345"),
|
||||
"zone": structpb.NewStringValue("us-central-1a"),
|
||||
"foo": structpb.NewBoolValue(true),
|
||||
"bar": structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "valid project and zone",
|
||||
in: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"project": structpb.NewStringValue("test-12345"),
|
||||
"zone": structpb.NewStringValue("us-central-1a"),
|
||||
},
|
||||
},
|
||||
expected: &CatalogAttributes{&cred.CredentialAttributes{
|
||||
Project: "test-12345",
|
||||
Zone: "us-central-1a",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := getCatalogAttributes(tc.in)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
require.Equal(status.Code(err), codes.InvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetSetAttributes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
in map[string]any
|
||||
normalized map[string]any
|
||||
expected *SetAttributes
|
||||
expectedErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing",
|
||||
in: map[string]any{},
|
||||
expected: &SetAttributes{},
|
||||
},
|
||||
{
|
||||
name: "valid example filter",
|
||||
in: map[string]any{
|
||||
ConstListInstancesFilter: "tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag",
|
||||
},
|
||||
expected: &SetAttributes{
|
||||
Filter: "tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid example instance group",
|
||||
in: map[string]any{
|
||||
ConstInstanceGroup: "test",
|
||||
},
|
||||
expected: &SetAttributes{
|
||||
InstanceGroup: "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown fields",
|
||||
in: map[string]any{
|
||||
"foo": true,
|
||||
"bar": true,
|
||||
},
|
||||
expectedErrContains: "attributes.bar: unrecognized field, attributes.foo: unrecognized field",
|
||||
},
|
||||
{
|
||||
name: "invalid, cannot have both instance filter and group defined",
|
||||
in: map[string]any{
|
||||
ConstListInstancesFilter: "test",
|
||||
ConstInstanceGroup: "test",
|
||||
},
|
||||
expectedErrContains: "Error in the attributes provided, cannot define both filter and instance group",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
input, err := structpb.NewStruct(tc.in)
|
||||
require.NoError(err)
|
||||
|
||||
actual, err := getSetAttributes(input)
|
||||
if tc.expectedErrContains != "" {
|
||||
require.Error(err)
|
||||
require.Contains(err.Error(), tc.expectedErrContains)
|
||||
require.Equal(status.Code(err), codes.InvalidArgument)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const (
|
||||
NumberMaxResults = uint32(100)
|
||||
)
|
||||
|
||||
// GoogleClient contains clients and attributes for authenticating to Google Cloud
|
||||
// and finding instances
|
||||
type GoogleClient struct {
|
||||
InstancesClient *compute.InstancesClient
|
||||
InstanceGroupClient *compute.InstanceGroupsClient
|
||||
Context context.Context
|
||||
Project string
|
||||
Zone string
|
||||
}
|
||||
|
||||
func (c *GoogleClient) getInstances(request *computepb.ListInstancesRequest) ([]*computepb.Instance, error) {
|
||||
hosts := []*computepb.Instance{}
|
||||
it := c.InstancesClient.List(c.Context, request)
|
||||
for {
|
||||
resp, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error listing instances: %s", err)
|
||||
}
|
||||
hosts = append(hosts, resp)
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func (c *GoogleClient) getInstancesForInstanceGroup(request *computepb.ListInstancesInstanceGroupsRequest) ([]*computepb.Instance, error) {
|
||||
hosts := []*computepb.Instance{}
|
||||
instances := c.InstanceGroupClient.ListInstances(c.Context, request)
|
||||
|
||||
for {
|
||||
resp, err := instances.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error listing instances for instance group %s: %s", request.InstanceGroup, err)
|
||||
}
|
||||
|
||||
instance, err := c.InstancesClient.Get(c.Context, &computepb.GetInstanceRequest{
|
||||
Instance: path.Base(resp.GetInstance()),
|
||||
Project: request.Project,
|
||||
Zone: request.Zone,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error getting instance %s for instance group %s: %s", resp.GetInstance(), request.InstanceGroup, err)
|
||||
}
|
||||
|
||||
hosts = append(hosts, instance)
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func instanceToHost(instance *computepb.Instance) (*pb.ListHostsResponseHost, error) {
|
||||
if instance.GetSelfLink() == "" {
|
||||
return nil, errors.New("response integrity error: missing instance self-link")
|
||||
}
|
||||
|
||||
if instance.GetName() == "" {
|
||||
return nil, errors.New("response integrity error: missing instance name")
|
||||
}
|
||||
|
||||
result := new(pb.ListHostsResponseHost)
|
||||
|
||||
result.ExternalId = instance.GetSelfLink()
|
||||
result.ExternalName = instance.GetName()
|
||||
|
||||
// Now go through all of the interfaces and log the IP address of
|
||||
// every interface.
|
||||
for _, iface := range instance.GetNetworkInterfaces() {
|
||||
// Populate default IP addresses/DNS name similar to how we do
|
||||
// for the entire instance.
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, iface.NetworkIP)
|
||||
|
||||
for _, external := range iface.AccessConfigs {
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, external.NatIP)
|
||||
}
|
||||
|
||||
// Add the IPv6 addresses.
|
||||
result.IpAddresses = appendDistinct(result.IpAddresses, iface.Ipv6Address)
|
||||
}
|
||||
|
||||
// Done
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// appendDistinct will append the elements to the slice
|
||||
// if an element is not nil, empty, and does not exist in slice.
|
||||
func appendDistinct(slice []string, elems ...*string) []string {
|
||||
for _, e := range elems {
|
||||
if e == nil || *e == "" || stringInSlice(slice, *e) {
|
||||
continue
|
||||
}
|
||||
slice = append(slice, *e)
|
||||
}
|
||||
return slice
|
||||
}
|
||||
|
||||
func stringInSlice(s []string, x string) bool {
|
||||
for _, y := range s {
|
||||
if x == y {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/compute/apiv1/computepb"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInstanceToHost(t *testing.T) {
|
||||
exampleId := "https://www.googleapis.com/compute/v1/projects/test-project/zones/us-central1-a/instances/test-instance"
|
||||
examplePrivateIp := "10.0.0.1"
|
||||
examplePrivateIp2 := "10.0.0.2"
|
||||
examplePublicIp := "1.1.1.1"
|
||||
examplePublicIp2 := "1.1.1.2"
|
||||
exampleIPv6 := "some::fake::address"
|
||||
exampleName := "test-instance"
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
instance *computepb.Instance
|
||||
expected *pb.ListHostsResponseHost
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "missing instance self-link",
|
||||
instance: &computepb.Instance{
|
||||
Name: &exampleName,
|
||||
},
|
||||
expectedErr: "response integrity error: missing instance self-link",
|
||||
},
|
||||
{
|
||||
name: "missing instance name",
|
||||
instance: &computepb.Instance{
|
||||
SelfLink: &exampleId,
|
||||
},
|
||||
expectedErr: "response integrity error: missing instance name",
|
||||
},
|
||||
{
|
||||
name: "good, single private IP with public IP address",
|
||||
instance: &computepb.Instance{
|
||||
SelfLink: &exampleId,
|
||||
Name: &exampleName,
|
||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||
{
|
||||
NetworkIP: &examplePrivateIp,
|
||||
AccessConfigs: []*computepb.AccessConfig{
|
||||
{
|
||||
NatIP: &examplePublicIp,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &pb.ListHostsResponseHost{
|
||||
ExternalId: exampleId,
|
||||
ExternalName: exampleName,
|
||||
IpAddresses: []string{examplePrivateIp, examplePublicIp},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good, single private IP address",
|
||||
instance: &computepb.Instance{
|
||||
SelfLink: &exampleId,
|
||||
Name: &exampleName,
|
||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||
{
|
||||
NetworkIP: &examplePrivateIp,
|
||||
AccessConfigs: []*computepb.AccessConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &pb.ListHostsResponseHost{
|
||||
ExternalId: exampleId,
|
||||
ExternalName: exampleName,
|
||||
IpAddresses: []string{examplePrivateIp},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good, multiple interfaces",
|
||||
instance: &computepb.Instance{
|
||||
SelfLink: &exampleId,
|
||||
Name: &exampleName,
|
||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||
{
|
||||
NetworkIP: &examplePrivateIp,
|
||||
AccessConfigs: []*computepb.AccessConfig{
|
||||
{
|
||||
NatIP: &examplePublicIp,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
NetworkIP: &examplePrivateIp2,
|
||||
AccessConfigs: []*computepb.AccessConfig{
|
||||
{
|
||||
NatIP: &examplePublicIp2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &pb.ListHostsResponseHost{
|
||||
ExternalId: exampleId,
|
||||
ExternalName: exampleName,
|
||||
IpAddresses: []string{examplePrivateIp, examplePublicIp, examplePrivateIp2, examplePublicIp2},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good, single private IP address with IPv6",
|
||||
instance: &computepb.Instance{
|
||||
SelfLink: &exampleId,
|
||||
Name: &exampleName,
|
||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||
{
|
||||
NetworkIP: &examplePrivateIp,
|
||||
Ipv6Address: &exampleIPv6,
|
||||
AccessConfigs: []*computepb.AccessConfig{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &pb.ListHostsResponseHost{
|
||||
ExternalId: exampleId,
|
||||
ExternalName: exampleName,
|
||||
IpAddresses: []string{examplePrivateIp, exampleIPv6},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
actual, err := instanceToHost(tc.instance)
|
||||
if tc.expectedErr != "" {
|
||||
require.EqualError(err, tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(tc.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
const (
|
||||
// ConstListInstancesFilter refers to a Google Cloud SDK filter to search for instances
|
||||
ConstListInstancesFilter = "filter"
|
||||
|
||||
// ConstInstanceGroup refers to the name of a Google Cloud instance group
|
||||
ConstInstanceGroup = "instance_group"
|
||||
)
|
||||
|
||||
var allowedSetFields = map[string]struct{}{
|
||||
ConstListInstancesFilter: {},
|
||||
ConstInstanceGroup: {},
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
compute "cloud.google.com/go/compute/apiv1"
|
||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||
errors "github.com/hashicorp/boundary-plugin-gcp/internal/errors"
|
||||
"github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// HostPlugin implements the HostPluginServiceServer interface for the
|
||||
// GCP host service plugin.
|
||||
type HostPlugin struct {
|
||||
pb.UnimplementedHostPluginServiceServer
|
||||
}
|
||||
|
||||
var (
|
||||
_ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
||||
)
|
||||
|
||||
// OnCreateCatalog is called when a dynamic host catalog is created.
|
||||
func (p *HostPlugin) OnCreateCatalog(_ context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||
}
|
||||
|
||||
attrs := catalog.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "attributes are required")
|
||||
}
|
||||
|
||||
if _, err := getCatalogAttributes(attrs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.OnCreateCatalogResponse{
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OnUpdateCatalog is called when a dynamic host catalog is updated.
|
||||
func (p *HostPlugin) OnUpdateCatalog(_ context.Context, req *pb.OnUpdateCatalogRequest) (*pb.OnUpdateCatalogResponse, error) {
|
||||
currentCatalog := req.GetCurrentCatalog()
|
||||
if currentCatalog == nil {
|
||||
return nil, status.Error(codes.FailedPrecondition, "current catalog is nil")
|
||||
}
|
||||
|
||||
secrets := req.GetNewCatalog().GetSecrets()
|
||||
if secrets == nil {
|
||||
// If new secrets weren't passed in, don't rotate what we have on
|
||||
// update.
|
||||
return &pb.OnUpdateCatalogResponse{}, nil
|
||||
}
|
||||
|
||||
return &pb.OnUpdateCatalogResponse{
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: nil,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OnDeleteCatalog is called when a dynamic host catalog is deleted.
|
||||
func (p *HostPlugin) OnDeleteCatalog(ctx context.Context, req *pb.OnDeleteCatalogRequest) (*pb.OnDeleteCatalogResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new catalog is nil")
|
||||
}
|
||||
|
||||
attrs := catalog.GetAttributes()
|
||||
if attrs == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "new catalog missing attributes")
|
||||
}
|
||||
|
||||
if _, err := getCatalogAttributes(attrs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.OnDeleteCatalogResponse{}, nil
|
||||
}
|
||||
|
||||
// OnCreateSet is called when a dynamic host set is created.
|
||||
func (p *HostPlugin) OnCreateSet(_ context.Context, req *pb.OnCreateSetRequest) (*pb.OnCreateSetResponse, error) {
|
||||
if err := validateSet(req.GetSet()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.OnCreateSetResponse{}, nil
|
||||
}
|
||||
|
||||
// OnUpdateSet is called when a dynamic host set is updated.
|
||||
func (p *HostPlugin) OnUpdateSet(_ context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
||||
if err := validateSet(req.GetNewSet()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.OnUpdateSetResponse{}, nil
|
||||
}
|
||||
|
||||
// OnDeleteSet is called when a dynamic host set is deleted.
|
||||
func (p *HostPlugin) OnDeleteSet(ctx context.Context, req *pb.OnDeleteSetRequest) (*pb.OnDeleteSetResponse, error) {
|
||||
return &pb.OnDeleteSetResponse{}, nil
|
||||
}
|
||||
|
||||
// ListHosts returns the list of instances based on filter or instance group name.
|
||||
func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*pb.ListHostsResponse, error) {
|
||||
catalog := req.GetCatalog()
|
||||
if catalog == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog is nil")
|
||||
}
|
||||
|
||||
catalogAttrsRaw := catalog.GetAttributes()
|
||||
if catalogAttrsRaw == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "catalog missing attributes")
|
||||
}
|
||||
|
||||
catalogAttributes, err := getCatalogAttributes(catalogAttrsRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sets := req.GetSets()
|
||||
if sets == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "sets is nil")
|
||||
}
|
||||
|
||||
type hostSetQuery struct {
|
||||
Id string
|
||||
InputInstances *computepb.ListInstancesRequest
|
||||
InputGroups *computepb.ListInstancesInstanceGroupsRequest
|
||||
Project string
|
||||
Zone string
|
||||
Output []*computepb.Instance
|
||||
OutputHosts []*pb.ListHostsResponseHost
|
||||
}
|
||||
|
||||
queries := make([]hostSetQuery, len(sets))
|
||||
for i, set := range sets {
|
||||
// Validate Id since we use it in output
|
||||
if set.GetId() == "" {
|
||||
return nil, status.Error(codes.InvalidArgument, "set missing id")
|
||||
}
|
||||
|
||||
if set.GetAttributes() == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
|
||||
}
|
||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if setAttrs.InstanceGroup != "" {
|
||||
queries[i] = hostSetQuery{
|
||||
Id: set.GetId(),
|
||||
InputGroups: buildListInstanceGroupsRequest(setAttrs, catalogAttributes),
|
||||
}
|
||||
} else {
|
||||
queries[i] = hostSetQuery{
|
||||
Id: set.GetId(),
|
||||
InputInstances: buildListInstancesRequest(setAttrs, catalogAttributes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
instancesClient, err := compute.NewInstancesRESTClient(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error creating NewInstancesRESTClient: %s", err)
|
||||
}
|
||||
|
||||
instanceGroupsClient, err := compute.NewInstanceGroupsRESTClient(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error creating NewInstanceGroupsRESTClient: %s", err)
|
||||
}
|
||||
|
||||
gclient := GoogleClient{
|
||||
InstancesClient: instancesClient,
|
||||
InstanceGroupClient: instanceGroupsClient,
|
||||
Context: ctx,
|
||||
}
|
||||
|
||||
// Run all queries now and assemble output.
|
||||
var maxLen int
|
||||
for i, query := range queries {
|
||||
var output []*computepb.Instance
|
||||
if query.InputGroups != nil {
|
||||
output, err = gclient.getInstancesForInstanceGroup(query.InputGroups)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error running getInstancesForInstanceGroup for host set id %q: %s", query.Id, err)
|
||||
}
|
||||
} else {
|
||||
output, err = gclient.getInstances(query.InputInstances)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error running getInstances for host set id %q: %s", query.Id, err)
|
||||
}
|
||||
}
|
||||
|
||||
queries[i].Output = output
|
||||
|
||||
// Process the output here, we will normalize this into a single
|
||||
// set of hosts afterwards (possibly removing duplicates).
|
||||
for _, instance := range output {
|
||||
host, err := instanceToHost(instance)
|
||||
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "error processing host results for host set id %q: %s", query.Id, err)
|
||||
}
|
||||
|
||||
queries[i].OutputHosts = append(queries[i].OutputHosts, host)
|
||||
maxLen++
|
||||
}
|
||||
}
|
||||
|
||||
// Now de-duplicate the hosts for the output. Maintain two sets:
|
||||
// * A slice of hosts that will be used in the output
|
||||
// * A map of hosts indexed by their external ID
|
||||
//
|
||||
// The map will is used in de-duplication to determine whether or
|
||||
// not we've seen the host before to simply add the query's set ID
|
||||
// to the list of set IDs that the host was seen in.
|
||||
hostResultSlice := make([]*pb.ListHostsResponseHost, 0, maxLen)
|
||||
hostResultMap := make(map[string]*pb.ListHostsResponseHost)
|
||||
for _, query := range queries {
|
||||
for _, host := range query.OutputHosts {
|
||||
if existingHost, ok := hostResultMap[host.ExternalId]; ok {
|
||||
// Existing host, just add the set ID to the list of seen IDs
|
||||
// and continue
|
||||
existingHost.SetIds = append(existingHost.SetIds, query.Id)
|
||||
continue
|
||||
}
|
||||
|
||||
// This will be the first seen entry, so append the set ID to
|
||||
// this host, and add it.
|
||||
host.SetIds = append(host.SetIds, query.Id)
|
||||
hostResultSlice = append(hostResultSlice, host)
|
||||
hostResultMap[host.ExternalId] = host
|
||||
}
|
||||
}
|
||||
|
||||
return &pb.ListHostsResponse{
|
||||
Hosts: hostResultSlice,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateSet(s *hostsets.HostSet) error {
|
||||
if s == nil {
|
||||
return status.Error(codes.InvalidArgument, "set is nil")
|
||||
}
|
||||
var attrs SetAttributes
|
||||
attrMap := s.GetAttributes().AsMap()
|
||||
if err := mapstructure.Decode(attrMap, &attrs); err != nil {
|
||||
return status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
|
||||
}
|
||||
|
||||
badFields := make(map[string]string)
|
||||
_, filterSet := attrMap[ConstListInstancesFilter]
|
||||
_, instanceGroupSet := attrMap[ConstInstanceGroup]
|
||||
|
||||
if instanceGroupSet && filterSet {
|
||||
badFields["attributes"] = "must set instance group or filter, cannot set both"
|
||||
} else if instanceGroupSet && len(attrs.InstanceGroup) == 0 {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstInstanceGroup)] = "must not be empty."
|
||||
} else if filterSet && len(attrs.Filter) == 0 {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty."
|
||||
}
|
||||
|
||||
for f := range attrMap {
|
||||
if _, ok := allowedSetFields[f]; !ok {
|
||||
badFields[fmt.Sprintf("attributes.%s", f)] = "Unrecognized field."
|
||||
}
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return errors.InvalidArgumentError("Invalid arguments in the new set", badFields)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
cred "github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||
"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/hashicorp/go-secure-stdlib/parseutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
"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) {
|
||||
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
||||
ctx := context.Background()
|
||||
p := &HostPlugin{}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, wd)
|
||||
project, err := parseutil.ParsePath("file://" + filepath.Join(wd, "secrets", "project"))
|
||||
require.NoError(t, err)
|
||||
zone, err := parseutil.ParsePath("file://" + filepath.Join(wd, "secrets", "zone"))
|
||||
require.NoError(t, err)
|
||||
|
||||
hostCatalogAttributes := &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
cred.ConstProject: project,
|
||||
cred.ConstZone: zone,
|
||||
}),
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.ListHostsRequest
|
||||
expected []*pb.ListHostsResponseHost
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "nil catalog",
|
||||
req: &pb.ListHostsRequest{},
|
||||
expectedErr: "catalog is nil",
|
||||
},
|
||||
{
|
||||
name: "project not defined",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
cred.ConstZone: zone,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "attributes.project: missing required value \"project\"",
|
||||
},
|
||||
{
|
||||
name: "get all three instances",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "get-all-instances",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*pb.ListHostsResponseHost{
|
||||
{
|
||||
Name: "boundary-0",
|
||||
},
|
||||
{
|
||||
Name: "boundary-1",
|
||||
},
|
||||
{
|
||||
Name: "boundary-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get one instance by name",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "get-one-instance-by-name",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstListInstancesFilter: "name = boundary-1",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*pb.ListHostsResponseHost{
|
||||
{
|
||||
Name: "boundary-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get instance group",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "get-instance-group",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstInstanceGroup: "boundary-servers",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*pb.ListHostsResponseHost{
|
||||
{
|
||||
Name: "boundary-0",
|
||||
},
|
||||
{
|
||||
Name: "boundary-1",
|
||||
},
|
||||
{
|
||||
Name: "boundary-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get two specific instances with two host sets",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "get-one-instance-by-name-0",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstListInstancesFilter: "name = boundary-0",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "get-one-instance-by-name-2",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstListInstancesFilter: "name = boundary-2",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*pb.ListHostsResponseHost{
|
||||
{
|
||||
Name: "boundary-0",
|
||||
},
|
||||
{
|
||||
Name: "boundary-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get one instance and an instance group in two host sets",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "get-one-instance-by-name",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstListInstancesFilter: "name = boundary-1",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "get-instance-group",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstInstanceGroup: "boundary-servers",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*pb.ListHostsResponseHost{
|
||||
{
|
||||
Name: "boundary-0",
|
||||
},
|
||||
{
|
||||
Name: "boundary-1",
|
||||
},
|
||||
{
|
||||
Name: "boundary-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get an instance group and one instance in two host sets",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "get-instance-group",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstInstanceGroup: "boundary-servers",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: "get-one-instance-by-name",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstListInstancesFilter: "name = boundary-2",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []*pb.ListHostsResponseHost{
|
||||
{
|
||||
Name: "boundary-0",
|
||||
},
|
||||
{
|
||||
Name: "boundary-1",
|
||||
},
|
||||
{
|
||||
Name: "boundary-2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid filter",
|
||||
req: &pb.ListHostsRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
Sets: []*hostsets.HostSet{
|
||||
{
|
||||
Id: "invalid-filter",
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
ConstListInstancesFilter: "not-a-filter",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "Invalid list filter expression",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := p.ListHosts(ctx, tc.req)
|
||||
if tc.expectedErr != "" {
|
||||
require.Contains(err.Error(), tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(len(tc.expected), len(actual.GetHosts()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCatalog(t *testing.T) {
|
||||
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
||||
ctx := context.Background()
|
||||
p := &HostPlugin{}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, wd)
|
||||
project, err := parseutil.ParsePath("file://" + filepath.Join(wd, "secrets", "project"))
|
||||
require.NoError(t, err)
|
||||
zone, err := parseutil.ParsePath("file://" + filepath.Join(wd, "secrets", "zone"))
|
||||
require.NoError(t, err)
|
||||
|
||||
hostCatalogAttributes := &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
cred.ConstProject: project,
|
||||
cred.ConstZone: zone,
|
||||
}),
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.OnCreateCatalogRequest
|
||||
expected *pb.HostCatalogPersisted
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
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: missing required value \"project\"",
|
||||
},
|
||||
{
|
||||
name: "do not persist secrets, use gcloud application default creds",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
},
|
||||
expected: &pb.HostCatalogPersisted{
|
||||
Secrets: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := p.OnCreateCatalog(ctx, tc.req)
|
||||
if tc.expectedErr != "" {
|
||||
require.Contains(err.Error(), tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Nil(actual.GetPersisted().Secrets)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCatalog(t *testing.T) {
|
||||
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
||||
ctx := context.Background()
|
||||
p := &HostPlugin{}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, wd)
|
||||
project, err := parseutil.ParsePath("file://" + filepath.Join(wd, "secrets", "project"))
|
||||
require.NoError(t, err)
|
||||
zone, err := parseutil.ParsePath("file://" + filepath.Join(wd, "secrets", "zone"))
|
||||
require.NoError(t, err)
|
||||
|
||||
hostCatalogAttributes := &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: wrapMap(t, map[string]interface{}{
|
||||
cred.ConstProject: project,
|
||||
cred.ConstZone: zone,
|
||||
}),
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.OnCreateCatalogRequest
|
||||
expected *pb.HostCatalogPersisted
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
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: missing required value \"project\"",
|
||||
},
|
||||
{
|
||||
name: "do not persist secrets, use gcloud ADC",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: hostCatalogAttributes,
|
||||
},
|
||||
},
|
||||
expected: &pb.HostCatalogPersisted{
|
||||
Secrets: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
actual, err := p.OnCreateCatalog(ctx, tc.req)
|
||||
if tc.expectedErr != "" {
|
||||
require.Contains(err.Error(), tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
require.Equal(actual.GetPersisted().Secrets, tc.expected.GetSecrets())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
p := &HostPlugin{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.OnCreateSetRequest
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "nil set",
|
||||
req: &pb.OnCreateSetRequest{},
|
||||
expectedErr: "set is nil",
|
||||
},
|
||||
{
|
||||
name: "nil set",
|
||||
req: &pb.OnCreateSetRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
cred.ConstProject: structpb.NewStringValue("test-project"),
|
||||
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "set is nil",
|
||||
},
|
||||
{
|
||||
name: "only allow instance group or filter, not both",
|
||||
req: &pb.OnCreateSetRequest{
|
||||
Set: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstListInstancesFilter: structpb.NewStringValue("status=RUNNING"),
|
||||
ConstInstanceGroup: structpb.NewStringValue("test"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "attributes: must set instance group or filter",
|
||||
},
|
||||
{
|
||||
name: "empty filter",
|
||||
req: &pb.OnCreateSetRequest{
|
||||
Set: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstListInstancesFilter: structpb.NewStringValue(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "attributes.filter: must not be empty",
|
||||
},
|
||||
{
|
||||
name: "empty instance group",
|
||||
req: &pb.OnCreateSetRequest{
|
||||
Set: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstInstanceGroup: structpb.NewStringValue(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "attributes.instance_group: must not be empty",
|
||||
},
|
||||
{
|
||||
name: "good filter",
|
||||
req: &pb.OnCreateSetRequest{
|
||||
Set: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstInstanceGroup: structpb.NewStringValue("status=RUNNING"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good instance group",
|
||||
req: &pb.OnCreateSetRequest{
|
||||
Set: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstInstanceGroup: structpb.NewStringValue("test"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
_, err := p.OnCreateSet(ctx, tc.req)
|
||||
if tc.expectedErr != "" {
|
||||
require.Contains(err.Error(), tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSet(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
p := &HostPlugin{}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pb.OnUpdateSetRequest
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "nil set",
|
||||
req: &pb.OnUpdateSetRequest{},
|
||||
expectedErr: "set is nil",
|
||||
},
|
||||
{
|
||||
name: "nil set",
|
||||
req: &pb.OnUpdateSetRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
cred.ConstProject: structpb.NewStringValue("test-project"),
|
||||
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "set is nil",
|
||||
},
|
||||
{
|
||||
name: "only allow instance group or filter, not both",
|
||||
req: &pb.OnUpdateSetRequest{
|
||||
NewSet: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstListInstancesFilter: structpb.NewStringValue("status=RUNNING"),
|
||||
ConstInstanceGroup: structpb.NewStringValue("test"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "attributes: must set instance group or filter",
|
||||
},
|
||||
{
|
||||
name: "empty filter",
|
||||
req: &pb.OnUpdateSetRequest{
|
||||
NewSet: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstListInstancesFilter: structpb.NewStringValue(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "attributes.filter: must not be empty",
|
||||
},
|
||||
{
|
||||
name: "empty instance group",
|
||||
req: &pb.OnUpdateSetRequest{
|
||||
NewSet: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstInstanceGroup: structpb.NewStringValue(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "attributes.instance_group: must not be empty",
|
||||
},
|
||||
{
|
||||
name: "good filter",
|
||||
req: &pb.OnUpdateSetRequest{
|
||||
NewSet: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstInstanceGroup: structpb.NewStringValue("status=RUNNING"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "good instance group",
|
||||
req: &pb.OnUpdateSetRequest{
|
||||
NewSet: &hostsets.HostSet{
|
||||
Attrs: &hostsets.HostSet_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
ConstInstanceGroup: structpb.NewStringValue("test"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
_, err := p.OnUpdateSet(ctx, tc.req)
|
||||
if tc.expectedErr != "" {
|
||||
require.Contains(err.Error(), tc.expectedErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user