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
+311
-282
No files matched your search
+15
-271
@@ -4,282 +4,26 @@
|
|||||||
package plugin
|
package plugin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"github.com/hashicorp/boundary-plugin-gcp/plugin/service/host"
|
||||||
"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"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"github.com/mitchellh/mapstructure"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GooglePlugin implements the GooglePluginServiceServer interface for the
|
// Ensure that HostPlugin implements the following services:
|
||||||
// Google host service plugin.
|
|
||||||
type GooglePlugin struct {
|
|
||||||
pb.UnimplementedHostPluginServiceServer
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ pb.HostPluginServiceServer = (*GooglePlugin)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// OnCreateCatalog is called when a dynamic host catalog is created.
|
|
||||||
func (p *GooglePlugin) 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 *GooglePlugin) 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 *GooglePlugin) 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 *GooglePlugin) 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 *GooglePlugin) 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 *GooglePlugin) 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 *GooglePlugin) 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
|
// HostPluginServiceServer
|
||||||
// not we've seen the host before to simply add the query's set ID
|
var (
|
||||||
// to the list of set IDs that the host was seen in.
|
_ pb.HostPluginServiceServer = (*host.HostPlugin)(nil)
|
||||||
hostResultSlice := make([]*pb.ListHostsResponseHost, 0, maxLen)
|
)
|
||||||
hostResultMap := make(map[string]*pb.ListHostsResponseHost)
|
|
||||||
for _, query := range queries {
|
// GCPPlugin contains a collection of all GCP plugin services.
|
||||||
for _, host := range query.OutputHosts {
|
type GCPPlugin struct {
|
||||||
if existingHost, ok := hostResultMap[host.ExternalId]; ok {
|
// HostPlugin implements the HostPluginServiceServer interface for
|
||||||
// Existing host, just add the set ID to the list of seen IDs
|
// supporting dynamically sourcing hosts from GCP Compute Engine.
|
||||||
// and continue
|
*host.HostPlugin
|
||||||
existingHost.SetIds = append(existingHost.SetIds, query.Id)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will be the first seen entry, so append the set ID to
|
func NewGCPPlugin() *GCPPlugin {
|
||||||
// this host, and add it.
|
return &GCPPlugin{
|
||||||
host.SetIds = append(host.SetIds, query.Id)
|
HostPlugin: &host.HostPlugin{},
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package plugin
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package plugin
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package plugin
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package plugin
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package plugin
|
package host
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ConstListInstancesFilter refers to a Google Cloud SDK filter to search for instances
|
// ConstListInstancesFilter refers to a Google Cloud SDK filter to search for instances
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package plugin
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -28,7 +28,7 @@ func wrapMap(t *testing.T, in map[string]interface{}) *structpb.Struct {
|
|||||||
func TestListHosts(t *testing.T) {
|
func TestListHosts(t *testing.T) {
|
||||||
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
p := &GooglePlugin{}
|
p := &HostPlugin{}
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -297,7 +297,7 @@ func TestListHosts(t *testing.T) {
|
|||||||
func TestCreateCatalog(t *testing.T) {
|
func TestCreateCatalog(t *testing.T) {
|
||||||
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
p := &GooglePlugin{}
|
p := &HostPlugin{}
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -378,7 +378,7 @@ func TestCreateCatalog(t *testing.T) {
|
|||||||
func TestUpdateCatalog(t *testing.T) {
|
func TestUpdateCatalog(t *testing.T) {
|
||||||
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
t.Skip("TODO: this needs a secrets file to run - maybe only manually?")
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
p := &GooglePlugin{}
|
p := &HostPlugin{}
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
wd, err := os.Getwd()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -458,7 +458,7 @@ func TestUpdateCatalog(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateSet(t *testing.T) {
|
func TestCreateSet(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
p := &GooglePlugin{}
|
p := &HostPlugin{}
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -580,7 +580,7 @@ func TestCreateSet(t *testing.T) {
|
|||||||
|
|
||||||
func TestUpdateSet(t *testing.T) {
|
func TestUpdateSet(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
p := &GooglePlugin{}
|
p := &HostPlugin{}
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
Reference in new issue
Block a user