* feat: OnUpdateSet Add support for OnUpdateSet with validation. OnUpdateSet will validate he attributes passed to it and run a dryRunValidation with the new set * feat: ListHosts (#14) - Update list hosts to only support listing instances - Update host client to support Instances API - Add reading IPV6 Address by `Ipv6AccessConfigs` - Add internal DNS which is the hostname
This commit is contained in:
1 parent
e4099a5f64
commit
779d383a24
7 files changed
+1014
-319
No files matched your search
@@ -61,8 +61,7 @@ func getCatalogAttributes(in *structpb.Struct) (*CatalogAttributes, error) {
|
|||||||
|
|
||||||
// SetAttributes defines attributes fro the host set
|
// SetAttributes defines attributes fro the host set
|
||||||
type SetAttributes struct {
|
type SetAttributes struct {
|
||||||
Filters []string `mapstructure:"filters"`
|
Filters []string `mapstructure:"filters"`
|
||||||
InstanceGroup string `mapstructure:"instance_group"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
||||||
@@ -78,7 +77,6 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete(unknownFields, ConstListInstancesFilter)
|
delete(unknownFields, ConstListInstancesFilter)
|
||||||
delete(unknownFields, ConstInstanceGroup)
|
|
||||||
|
|
||||||
for a := range unknownFields {
|
for a := range unknownFields {
|
||||||
badFields[fmt.Sprintf("attributes.%s", a)] = "unrecognized field"
|
badFields[fmt.Sprintf("attributes.%s", a)] = "unrecognized field"
|
||||||
@@ -98,13 +96,6 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if instanceGroupRaw, ok := inMap[ConstInstanceGroup]; ok {
|
|
||||||
switch instanceGroupValue := instanceGroupRaw.(type) {
|
|
||||||
case string:
|
|
||||||
inMap[ConstInstanceGroup] = string(instanceGroupValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := mapstructure.Decode(inMap, &setAttrs); err != nil {
|
if err := mapstructure.Decode(inMap, &setAttrs); err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error decoding set attributes: %s", err)
|
||||||
}
|
}
|
||||||
@@ -112,31 +103,21 @@ func getSetAttributes(in *structpb.Struct) (*SetAttributes, error) {
|
|||||||
return &setAttrs, nil
|
return &setAttrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildListInstancesRequest(attributes *SetAttributes, catalog *CatalogAttributes) *computepb.ListInstancesRequest {
|
func buildListInstancesRequest(attributes *SetAttributes, catalog *CatalogAttributes) (*computepb.ListInstancesRequest, error) {
|
||||||
request := &computepb.ListInstancesRequest{
|
request := &computepb.ListInstancesRequest{
|
||||||
Project: catalog.ProjectId,
|
Project: catalog.ProjectId,
|
||||||
Zone: catalog.Zone,
|
Zone: catalog.Zone,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(attributes.Filters) > 1 {
|
filters, err := buildFilters(attributes)
|
||||||
filters := strings.Join(attributes.Filters, " ")
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error building filters: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filters) > 1 {
|
||||||
|
filters := strings.Join(filters, " AND ")
|
||||||
request.Filter = &filters
|
request.Filter = &filters
|
||||||
}
|
}
|
||||||
|
|
||||||
return request
|
return request, nil
|
||||||
}
|
|
||||||
|
|
||||||
func buildListInstanceGroupsRequest(attributes *SetAttributes, catalog *CatalogAttributes) *computepb.ListInstancesInstanceGroupsRequest {
|
|
||||||
request := &computepb.ListInstancesInstanceGroupsRequest{
|
|
||||||
InstanceGroup: attributes.InstanceGroup,
|
|
||||||
Project: catalog.ProjectId,
|
|
||||||
Zone: catalog.Zone,
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(attributes.Filters) > 1 {
|
|
||||||
filters := strings.Join(attributes.Filters, " ")
|
|
||||||
request.Filter = &filters
|
|
||||||
}
|
|
||||||
|
|
||||||
return request
|
|
||||||
}
|
}
|
||||||
@@ -95,15 +95,6 @@ func TestGetSetAttributes(t *testing.T) {
|
|||||||
Filters: []string{"tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag"},
|
Filters: []string{"tags.items=my-tag AND -tags.items=my-other-tag) OR tags.items=alternative-tag"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "valid example instance group",
|
|
||||||
in: map[string]any{
|
|
||||||
ConstInstanceGroup: "test",
|
|
||||||
},
|
|
||||||
expected: &SetAttributes{
|
|
||||||
InstanceGroup: "test",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "unknown fields",
|
name: "unknown fields",
|
||||||
in: map[string]any{
|
in: map[string]any{
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ package host
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"path"
|
"strconv"
|
||||||
|
|
||||||
compute "cloud.google.com/go/compute/apiv1"
|
|
||||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"google.golang.org/api/iterator"
|
"google.golang.org/api/iterator"
|
||||||
@@ -16,66 +15,25 @@ import (
|
|||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
func getInstances(ctx context.Context, instancesClient InstancesAPI, request *computepb.ListInstancesRequest) ([]*computepb.Instance, error) {
|
||||||
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{}
|
hosts := []*computepb.Instance{}
|
||||||
it := c.InstancesClient.List(c.Context, request)
|
it := instancesClient.List(ctx, request)
|
||||||
for {
|
for {
|
||||||
resp, err := it.Next()
|
resp, err := it.Next()
|
||||||
if err == iterator.Done {
|
if err == iterator.Done {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "error listing instances: %s", err)
|
return nil, status.Errorf(codes.Unknown, "error listing instances: %s", err)
|
||||||
}
|
}
|
||||||
hosts = append(hosts, resp)
|
hosts = append(hosts, resp)
|
||||||
}
|
}
|
||||||
return hosts, nil
|
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) {
|
func instanceToHost(instance *computepb.Instance) (*pb.ListHostsResponseHost, error) {
|
||||||
if instance.GetSelfLink() == "" {
|
if instance.GetId() == 0 {
|
||||||
return nil, errors.New("response integrity error: missing instance self-link")
|
return nil, errors.New("response integrity error: missing instance id")
|
||||||
}
|
}
|
||||||
|
|
||||||
if instance.GetName() == "" {
|
if instance.GetName() == "" {
|
||||||
@@ -84,22 +42,33 @@ func instanceToHost(instance *computepb.Instance) (*pb.ListHostsResponseHost, er
|
|||||||
|
|
||||||
result := new(pb.ListHostsResponseHost)
|
result := new(pb.ListHostsResponseHost)
|
||||||
|
|
||||||
result.ExternalId = instance.GetSelfLink()
|
result.ExternalId = strconv.FormatUint(instance.GetId(), 10)
|
||||||
result.ExternalName = instance.GetName()
|
result.ExternalName = instance.GetName()
|
||||||
|
|
||||||
|
// Internal DNS name is the hostname of the instance.
|
||||||
|
// https://cloud.google.com/compute/docs/networking/using-internal-dns
|
||||||
|
dnsName := instance.GetHostname()
|
||||||
|
result.DnsNames = appendDistinct(result.DnsNames, &dnsName)
|
||||||
|
|
||||||
// Now go through all of the interfaces and log the IP address of
|
// Now go through all of the interfaces and log the IP address of
|
||||||
// every interface.
|
// every interface.
|
||||||
for _, iface := range instance.GetNetworkInterfaces() {
|
for _, iface := range instance.GetNetworkInterfaces() {
|
||||||
// Populate default IP addresses/DNS name similar to how we do
|
// Populate default IP addresses name similar to how we do
|
||||||
// for the entire instance.
|
// for the entire instance.
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, iface.NetworkIP)
|
result.IpAddresses = appendDistinct(result.IpAddresses, iface.NetworkIP)
|
||||||
|
|
||||||
for _, external := range iface.AccessConfigs {
|
for _, external := range iface.AccessConfigs {
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, external.NatIP)
|
result.IpAddresses = appendDistinct(result.IpAddresses, external.NatIP)
|
||||||
|
result.IpAddresses = appendDistinct(result.IpAddresses, external.ExternalIpv6)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the IPv6 addresses.
|
// Add the IPv6 addresses.
|
||||||
result.IpAddresses = appendDistinct(result.IpAddresses, iface.Ipv6Address)
|
result.IpAddresses = appendDistinct(result.IpAddresses, iface.Ipv6Address)
|
||||||
|
if iface.Ipv6AccessConfigs != nil {
|
||||||
|
for _, external := range iface.Ipv6AccessConfigs {
|
||||||
|
result.IpAddresses = appendDistinct(result.IpAddresses, external.ExternalIpv6)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Done
|
// Done
|
||||||
|
|||||||
@@ -4,15 +4,19 @@
|
|||||||
package host
|
package host
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"cloud.google.com/go/compute/apiv1/computepb"
|
"cloud.google.com/go/compute/apiv1/computepb"
|
||||||
|
"github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestInstanceToHost(t *testing.T) {
|
func TestInstanceToHost(t *testing.T) {
|
||||||
exampleId := "https://www.googleapis.com/compute/v1/projects/test-project/zones/us-central1-a/instances/test-instance"
|
exampleId := uint64(123456789)
|
||||||
examplePrivateIp := "10.0.0.1"
|
examplePrivateIp := "10.0.0.1"
|
||||||
examplePrivateIp2 := "10.0.0.2"
|
examplePrivateIp2 := "10.0.0.2"
|
||||||
examplePublicIp := "1.1.1.1"
|
examplePublicIp := "1.1.1.1"
|
||||||
@@ -27,24 +31,24 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
expectedErr string
|
expectedErr string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "missing instance self-link",
|
name: "missing instance id",
|
||||||
instance: &computepb.Instance{
|
instance: &computepb.Instance{
|
||||||
Name: &exampleName,
|
Name: &exampleName,
|
||||||
},
|
},
|
||||||
expectedErr: "response integrity error: missing instance self-link",
|
expectedErr: "response integrity error: missing instance id",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing instance name",
|
name: "missing instance name",
|
||||||
instance: &computepb.Instance{
|
instance: &computepb.Instance{
|
||||||
SelfLink: &exampleId,
|
Id: &exampleId,
|
||||||
},
|
},
|
||||||
expectedErr: "response integrity error: missing instance name",
|
expectedErr: "response integrity error: missing instance name",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "good, single private IP with public IP address",
|
name: "good, single private IP with public IP address",
|
||||||
instance: &computepb.Instance{
|
instance: &computepb.Instance{
|
||||||
SelfLink: &exampleId,
|
Id: &exampleId,
|
||||||
Name: &exampleName,
|
Name: &exampleName,
|
||||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||||
{
|
{
|
||||||
NetworkIP: &examplePrivateIp,
|
NetworkIP: &examplePrivateIp,
|
||||||
@@ -57,7 +61,7 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &pb.ListHostsResponseHost{
|
expected: &pb.ListHostsResponseHost{
|
||||||
ExternalId: exampleId,
|
ExternalId: strconv.FormatUint(exampleId, 10),
|
||||||
ExternalName: exampleName,
|
ExternalName: exampleName,
|
||||||
IpAddresses: []string{examplePrivateIp, examplePublicIp},
|
IpAddresses: []string{examplePrivateIp, examplePublicIp},
|
||||||
},
|
},
|
||||||
@@ -65,8 +69,8 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "good, single private IP address",
|
name: "good, single private IP address",
|
||||||
instance: &computepb.Instance{
|
instance: &computepb.Instance{
|
||||||
SelfLink: &exampleId,
|
Id: &exampleId,
|
||||||
Name: &exampleName,
|
Name: &exampleName,
|
||||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||||
{
|
{
|
||||||
NetworkIP: &examplePrivateIp,
|
NetworkIP: &examplePrivateIp,
|
||||||
@@ -75,7 +79,7 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &pb.ListHostsResponseHost{
|
expected: &pb.ListHostsResponseHost{
|
||||||
ExternalId: exampleId,
|
ExternalId: strconv.FormatUint(exampleId, 10),
|
||||||
ExternalName: exampleName,
|
ExternalName: exampleName,
|
||||||
IpAddresses: []string{examplePrivateIp},
|
IpAddresses: []string{examplePrivateIp},
|
||||||
},
|
},
|
||||||
@@ -83,8 +87,8 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "good, multiple interfaces",
|
name: "good, multiple interfaces",
|
||||||
instance: &computepb.Instance{
|
instance: &computepb.Instance{
|
||||||
SelfLink: &exampleId,
|
Id: &exampleId,
|
||||||
Name: &exampleName,
|
Name: &exampleName,
|
||||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||||
{
|
{
|
||||||
NetworkIP: &examplePrivateIp,
|
NetworkIP: &examplePrivateIp,
|
||||||
@@ -105,7 +109,7 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &pb.ListHostsResponseHost{
|
expected: &pb.ListHostsResponseHost{
|
||||||
ExternalId: exampleId,
|
ExternalId: strconv.FormatUint(exampleId, 10),
|
||||||
ExternalName: exampleName,
|
ExternalName: exampleName,
|
||||||
IpAddresses: []string{examplePrivateIp, examplePublicIp, examplePrivateIp2, examplePublicIp2},
|
IpAddresses: []string{examplePrivateIp, examplePublicIp, examplePrivateIp2, examplePublicIp2},
|
||||||
},
|
},
|
||||||
@@ -113,8 +117,8 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "good, single private IP address with IPv6",
|
name: "good, single private IP address with IPv6",
|
||||||
instance: &computepb.Instance{
|
instance: &computepb.Instance{
|
||||||
SelfLink: &exampleId,
|
Id: &exampleId,
|
||||||
Name: &exampleName,
|
Name: &exampleName,
|
||||||
NetworkInterfaces: []*computepb.NetworkInterface{
|
NetworkInterfaces: []*computepb.NetworkInterface{
|
||||||
{
|
{
|
||||||
NetworkIP: &examplePrivateIp,
|
NetworkIP: &examplePrivateIp,
|
||||||
@@ -124,7 +128,7 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
expected: &pb.ListHostsResponseHost{
|
expected: &pb.ListHostsResponseHost{
|
||||||
ExternalId: exampleId,
|
ExternalId: strconv.FormatUint(exampleId, 10),
|
||||||
ExternalName: exampleName,
|
ExternalName: exampleName,
|
||||||
IpAddresses: []string{examplePrivateIp, exampleIPv6},
|
IpAddresses: []string{examplePrivateIp, exampleIPv6},
|
||||||
},
|
},
|
||||||
@@ -146,3 +150,88 @@ func TestInstanceToHost(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetInstances(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
exampleId := uint64(123456789)
|
||||||
|
exampleName := "test-instance"
|
||||||
|
exampleInstance := &computepb.Instance{
|
||||||
|
Id: &exampleId,
|
||||||
|
Name: &exampleName,
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
instancesClient InstancesAPI
|
||||||
|
request *computepb.ListInstancesRequest
|
||||||
|
expected []*computepb.Instance
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "successful retrieval",
|
||||||
|
instancesClient: func() InstancesAPI {
|
||||||
|
fn := newTestMockInstances(ctx,
|
||||||
|
nil,
|
||||||
|
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{
|
||||||
|
Items: []*computepb.Instance{exampleInstance},
|
||||||
|
}),
|
||||||
|
testMockInstancesWithListInstancesError(nil),
|
||||||
|
)
|
||||||
|
client, err := fn(&credential.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return client
|
||||||
|
}(),
|
||||||
|
request: &computepb.ListInstancesRequest{},
|
||||||
|
expected: []*computepb.Instance{exampleInstance},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error during listing",
|
||||||
|
instancesClient: func() InstancesAPI {
|
||||||
|
fn := newTestMockInstances(ctx,
|
||||||
|
nil,
|
||||||
|
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
|
||||||
|
testMockInstancesWithListInstancesError(errors.New("internal error")),
|
||||||
|
)
|
||||||
|
client, err := fn(&credential.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return client
|
||||||
|
}(),
|
||||||
|
request: &computepb.ListInstancesRequest{},
|
||||||
|
expectedErr: "error listing instances",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no instances",
|
||||||
|
instancesClient: func() InstancesAPI {
|
||||||
|
fn := newTestMockInstances(ctx,
|
||||||
|
nil,
|
||||||
|
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
|
||||||
|
testMockInstancesWithListInstancesError(nil),
|
||||||
|
)
|
||||||
|
client, err := fn(&credential.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
return client
|
||||||
|
}(),
|
||||||
|
request: &computepb.ListInstancesRequest{},
|
||||||
|
expected: []*computepb.Instance{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
actual, err := getInstances(ctx, tc.instancesClient, tc.request)
|
||||||
|
if tc.expectedErr != "" {
|
||||||
|
require.ErrorContains(err, tc.expectedErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(err)
|
||||||
|
require.Len(actual, len(tc.expected))
|
||||||
|
if len(actual) > 0 {
|
||||||
|
require.Equal(tc.expected[0].Name, actual[0].Name)
|
||||||
|
require.Equal(tc.expected[0].Id, actual[0].Id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
compute "cloud.google.com/go/compute/apiv1"
|
|
||||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||||
"github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
"github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||||
errors "github.com/hashicorp/boundary-plugin-gcp/internal/errors"
|
errors "github.com/hashicorp/boundary-plugin-gcp/internal/errors"
|
||||||
@@ -38,6 +37,13 @@ var (
|
|||||||
_ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
_ pb.HostPluginServiceServer = (*HostPlugin)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type hostSetQuery struct {
|
||||||
|
Id string
|
||||||
|
InputInstances *computepb.ListInstancesRequest
|
||||||
|
Output []*computepb.Instance
|
||||||
|
OutputHosts []*pb.ListHostsResponseHost
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// defaultStatusFilter is the default filter to apply to instances
|
// defaultStatusFilter is the default filter to apply to instances
|
||||||
// if no status filter is provided.
|
// if no status filter is provided.
|
||||||
@@ -380,10 +386,64 @@ func (p *HostPlugin) OnCreateSet(ctx context.Context, req *pb.OnCreateSetRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OnUpdateSet is called when a dynamic host set is updated.
|
// OnUpdateSet is called when a dynamic host set is updated.
|
||||||
func (p *HostPlugin) OnUpdateSet(_ context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
func (p *HostPlugin) OnUpdateSet(ctx context.Context, req *pb.OnUpdateSetRequest) (*pb.OnUpdateSetResponse, error) {
|
||||||
if err := validateSet(req.GetNewSet()); err != nil {
|
catalog := req.GetCatalog()
|
||||||
|
if catalog == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "catalog is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := catalog.GetAttributes()
|
||||||
|
if attrs == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "catalog attributes are required")
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogAttributes, err := getCatalogAttributes(attrs)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
credState, err := credential.PersistedStateFromProto(
|
||||||
|
req.GetPersisted().GetSecrets(),
|
||||||
|
catalogAttributes.CredentialAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error getting persisted state from proto: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogState, err := newGCPCatalogPersistedState(
|
||||||
|
append([]gcpCatalogPersistedStateOption{
|
||||||
|
withCredentials(credState),
|
||||||
|
}, p.testCatalogStateOpts...)...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
set := req.GetNewSet()
|
||||||
|
if set == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "set is required")
|
||||||
|
}
|
||||||
|
if err := validateSet(set); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if set.GetAttributes() == nil {
|
||||||
|
return nil, status.Error(codes.InvalidArgument, "set attributes are required")
|
||||||
|
}
|
||||||
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
listInstanceFilters, err := buildFilters(setAttrs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error building filters: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// perform dry run to ensure we can interact with GCP as expected.
|
||||||
|
if st := dryRunValidation(ctx, catalogState, listInstanceFilters, p.testGCPClientOpts...); st != nil {
|
||||||
|
return nil, st.Err()
|
||||||
|
}
|
||||||
|
|
||||||
return &pb.OnUpdateSetResponse{}, nil
|
return &pb.OnUpdateSetResponse{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,21 +469,27 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
credState, err := credential.PersistedStateFromProto(
|
||||||
|
req.GetPersisted().GetSecrets(),
|
||||||
|
catalogAttributes.CredentialAttributes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error getting persisted state from proto: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
catalogState, err := newGCPCatalogPersistedState(
|
||||||
|
append([]gcpCatalogPersistedStateOption{
|
||||||
|
withCredentials(credState),
|
||||||
|
}, p.testCatalogStateOpts...)...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.InvalidArgument, "error loading persisted state: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
sets := req.GetSets()
|
sets := req.GetSets()
|
||||||
if sets == nil {
|
if sets == nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "sets is 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))
|
queries := make([]hostSetQuery, len(sets))
|
||||||
for i, set := range sets {
|
for i, set := range sets {
|
||||||
// Validate Id since we use it in output
|
// Validate Id since we use it in output
|
||||||
@@ -432,56 +498,35 @@ func (p *HostPlugin) ListHosts(ctx context.Context, req *pb.ListHostsRequest) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
if set.GetAttributes() == nil {
|
if set.GetAttributes() == nil {
|
||||||
return nil, status.Error(codes.InvalidArgument, "set missing attributes")
|
return nil, status.Error(codes.InvalidArgument, fmt.Sprintf("set %s missing attributes", set.GetId()))
|
||||||
}
|
}
|
||||||
setAttrs, err := getSetAttributes(set.GetAttributes())
|
setAttrs, err := getSetAttributes(set.GetAttributes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if setAttrs.InstanceGroup != "" {
|
input, err := buildListInstancesRequest(setAttrs, catalogAttributes)
|
||||||
queries[i] = hostSetQuery{
|
if err != nil {
|
||||||
Id: set.GetId(),
|
return nil, status.Errorf(codes.InvalidArgument, "error building list instances request: %s", err)
|
||||||
InputGroups: buildListInstanceGroupsRequest(setAttrs, catalogAttributes),
|
}
|
||||||
}
|
|
||||||
} else {
|
queries[i] = hostSetQuery{
|
||||||
queries[i] = hostSetQuery{
|
Id: set.GetId(),
|
||||||
Id: set.GetId(),
|
InputInstances: input,
|
||||||
InputInstances: buildListInstancesRequest(setAttrs, catalogAttributes),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
instancesClient, err := compute.NewInstancesRESTClient(ctx)
|
instancesClient, err := catalogState.InstancesClient(ctx, p.testGCPClientOpts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "error creating NewInstancesRESTClient: %s", err)
|
return nil, status.Errorf(codes.InvalidArgument, "error getting instances client: %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.
|
// Run all queries now and assemble output.
|
||||||
var maxLen int
|
var maxLen int
|
||||||
for i, query := range queries {
|
for i, query := range queries {
|
||||||
var output []*computepb.Instance
|
output, err := getInstances(ctx, instancesClient, query.InputInstances)
|
||||||
if query.InputGroups != nil {
|
if err != nil {
|
||||||
output, err = gclient.getInstancesForInstanceGroup(query.InputGroups)
|
return nil, status.Errorf(codes.InvalidArgument, "error running list instances for host set id %q: %s", query.Id, err)
|
||||||
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
|
queries[i].Output = output
|
||||||
@@ -547,8 +592,6 @@ func validateSet(s *hostsets.HostSet) error {
|
|||||||
|
|
||||||
if instanceGroupSet && filterSet {
|
if instanceGroupSet && filterSet {
|
||||||
badFields["attributes"] = "must set instance group or filter, cannot set both"
|
badFields["attributes"] = "must set instance group or filter, cannot set both"
|
||||||
} else if instanceGroupSet && len(attrs.InstanceGroup) == 0 {
|
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstInstanceGroup)] = "must not be empty"
|
|
||||||
} else if filterSet && len(attrs.Filters) == 0 {
|
} else if filterSet && len(attrs.Filters) == 0 {
|
||||||
badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty"
|
badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty"
|
||||||
}
|
}
|
||||||
|
|||||||
+787
-161
File diff suppressed because it is too large.
Load diff
@@ -17,10 +17,6 @@ import (
|
|||||||
"google.golang.org/protobuf/types/known/structpb"
|
"google.golang.org/protobuf/types/known/structpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Iterator interface {
|
|
||||||
Next() (*computepb.Instance, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type InstancesAPI interface {
|
type InstancesAPI interface {
|
||||||
List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) *compute.InstanceIterator
|
List(ctx context.Context, req *computepb.ListInstancesRequest, opts ...gax.CallOption) *compute.InstanceIterator
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user