Move cli/gcp to cli/internal/gcp

This commit is contained in:
katexochen 2022-06-07 14:52:47 +02:00 committed by Paul Meyer
parent 48b4f10207
commit 6cd93e4179
27 changed files with 20 additions and 91 deletions

View file

@ -0,0 +1,7 @@
package gcp
import "fmt"
func AutoscalingNodeGroup(project string, zone string, nodeInstanceGroup string, min int, max int) string {
return fmt.Sprintf("%d:%d:https://www.googleapis.com/compute/v1/projects/%s/zones/%s/instanceGroups/%s", min, max, project, zone, nodeInstanceGroup)
}

View file

@ -0,0 +1,14 @@
package gcp
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAutoscalingNodeGroup(t *testing.T) {
assert := assert.New(t)
nodeGroups := AutoscalingNodeGroup("some-project", "some-zone", "some-group", 0, 100)
wantNodeGroups := "0:100:https://www.googleapis.com/compute/v1/projects/some-project/zones/some-zone/instanceGroups/some-group"
assert.Equal(wantNodeGroups, nodeGroups)
}

View file

@ -0,0 +1,101 @@
package client
import (
"context"
"github.com/googleapis/gax-go/v2"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
adminpb "google.golang.org/genproto/googleapis/iam/admin/v1"
iampb "google.golang.org/genproto/googleapis/iam/v1"
)
type instanceAPI interface {
Close() error
List(ctx context.Context, req *computepb.ListInstancesRequest,
opts ...gax.CallOption) InstanceIterator
}
type operationRegionAPI interface {
Close() error
Wait(ctx context.Context, req *computepb.WaitRegionOperationRequest,
opts ...gax.CallOption) (*computepb.Operation, error)
}
type operationZoneAPI interface {
Close() error
Wait(ctx context.Context, req *computepb.WaitZoneOperationRequest,
opts ...gax.CallOption) (*computepb.Operation, error)
}
type operationGlobalAPI interface {
Close() error
Wait(ctx context.Context, req *computepb.WaitGlobalOperationRequest,
opts ...gax.CallOption) (*computepb.Operation, error)
}
type firewallsAPI interface {
Close() error
Delete(ctx context.Context, req *computepb.DeleteFirewallRequest,
opts ...gax.CallOption) (Operation, error)
Insert(ctx context.Context, req *computepb.InsertFirewallRequest,
opts ...gax.CallOption) (Operation, error)
}
type networksAPI interface {
Close() error
Delete(ctx context.Context, req *computepb.DeleteNetworkRequest,
opts ...gax.CallOption) (Operation, error)
Insert(ctx context.Context, req *computepb.InsertNetworkRequest,
opts ...gax.CallOption) (Operation, error)
}
type subnetworksAPI interface {
Close() error
Delete(ctx context.Context, req *computepb.DeleteSubnetworkRequest,
opts ...gax.CallOption) (Operation, error)
Insert(ctx context.Context, req *computepb.InsertSubnetworkRequest,
opts ...gax.CallOption) (Operation, error)
}
type instanceTemplateAPI interface {
Close() error
Delete(ctx context.Context, req *computepb.DeleteInstanceTemplateRequest,
opts ...gax.CallOption) (Operation, error)
Insert(ctx context.Context, req *computepb.InsertInstanceTemplateRequest,
opts ...gax.CallOption) (Operation, error)
}
type instanceGroupManagersAPI interface {
Close() error
Delete(ctx context.Context, req *computepb.DeleteInstanceGroupManagerRequest,
opts ...gax.CallOption) (Operation, error)
Insert(ctx context.Context, req *computepb.InsertInstanceGroupManagerRequest,
opts ...gax.CallOption) (Operation, error)
ListManagedInstances(ctx context.Context, req *computepb.ListManagedInstancesInstanceGroupManagersRequest,
opts ...gax.CallOption) ManagedInstanceIterator
}
type iamAPI interface {
Close() error
CreateServiceAccount(ctx context.Context, req *adminpb.CreateServiceAccountRequest, opts ...gax.CallOption) (*adminpb.ServiceAccount, error)
CreateServiceAccountKey(ctx context.Context, req *adminpb.CreateServiceAccountKeyRequest, opts ...gax.CallOption) (*adminpb.ServiceAccountKey, error)
DeleteServiceAccount(ctx context.Context, req *adminpb.DeleteServiceAccountRequest, opts ...gax.CallOption) error
}
type projectsAPI interface {
Close() error
GetIamPolicy(ctx context.Context, req *iampb.GetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error)
SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error)
}
type Operation interface {
Proto() *computepb.Operation
}
type ManagedInstanceIterator interface {
Next() (*computepb.ManagedInstance, error)
}
type InstanceIterator interface {
Next() (*computepb.Instance, error)
}

View file

@ -0,0 +1,413 @@
package client
import (
"context"
"time"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
adminpb "google.golang.org/genproto/googleapis/iam/admin/v1"
iampb "google.golang.org/genproto/googleapis/iam/v1"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
type stubOperation struct {
*computepb.Operation
}
func (o *stubOperation) Proto() *computepb.Operation {
return o.Operation
}
type stubInstanceAPI struct {
listIterator *stubInstanceIterator
}
func (a stubInstanceAPI) Close() error {
return nil
}
func (a stubInstanceAPI) List(ctx context.Context, req *computepb.ListInstancesRequest,
opts ...gax.CallOption,
) InstanceIterator {
return a.listIterator
}
type stubInstanceIterator struct {
instances []*computepb.Instance
nextErr error
internalCounter int
}
func (i *stubInstanceIterator) Next() (*computepb.Instance, error) {
if i.nextErr != nil {
return nil, i.nextErr
}
if i.internalCounter >= len(i.instances) {
i.internalCounter = 0
return nil, iterator.Done
}
resp := i.instances[i.internalCounter]
i.internalCounter++
return resp, nil
}
type stubOperationZoneAPI struct {
waitErr error
}
func (a stubOperationZoneAPI) Close() error {
return nil
}
func (a stubOperationZoneAPI) Wait(ctx context.Context, req *computepb.WaitZoneOperationRequest,
opts ...gax.CallOption,
) (*computepb.Operation, error) {
if a.waitErr != nil {
return nil, a.waitErr
}
return &computepb.Operation{
Status: computepb.Operation_DONE.Enum(),
}, nil
}
type stubOperationRegionAPI struct {
waitErr error
}
func (a stubOperationRegionAPI) Close() error {
return nil
}
func (a stubOperationRegionAPI) Wait(ctx context.Context, req *computepb.WaitRegionOperationRequest,
opts ...gax.CallOption,
) (*computepb.Operation, error) {
if a.waitErr != nil {
return nil, a.waitErr
}
return &computepb.Operation{
Status: computepb.Operation_DONE.Enum(),
}, nil
}
type stubOperationGlobalAPI struct {
waitErr error
}
func (a stubOperationGlobalAPI) Close() error {
return nil
}
func (a stubOperationGlobalAPI) Wait(ctx context.Context, req *computepb.WaitGlobalOperationRequest,
opts ...gax.CallOption,
) (*computepb.Operation, error) {
if a.waitErr != nil {
return nil, a.waitErr
}
return &computepb.Operation{
Status: computepb.Operation_DONE.Enum(),
}, nil
}
type stubFirewallsAPI struct {
deleteErr error
insertErr error
}
func (a stubFirewallsAPI) Close() error {
return nil
}
func (a stubFirewallsAPI) Delete(ctx context.Context, req *computepb.DeleteFirewallRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.deleteErr != nil {
return nil, a.deleteErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
},
}, nil
}
func (a stubFirewallsAPI) Insert(ctx context.Context, req *computepb.InsertFirewallRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.insertErr != nil {
return nil, a.insertErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
},
}, nil
}
type stubNetworksAPI struct {
insertErr error
deleteErr error
}
func (a stubNetworksAPI) Close() error {
return nil
}
func (a stubNetworksAPI) Insert(ctx context.Context, req *computepb.InsertNetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.insertErr != nil {
return nil, a.insertErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
},
}, nil
}
func (a stubNetworksAPI) Delete(ctx context.Context, req *computepb.DeleteNetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.deleteErr != nil {
return nil, a.deleteErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
},
}, nil
}
type stubSubnetworksAPI struct {
insertErr error
deleteErr error
}
func (a stubSubnetworksAPI) Close() error {
return nil
}
func (a stubSubnetworksAPI) Insert(ctx context.Context, req *computepb.InsertSubnetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.insertErr != nil {
return nil, a.insertErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
Region: proto.String("region"),
},
}, nil
}
func (a stubSubnetworksAPI) Delete(ctx context.Context, req *computepb.DeleteSubnetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.deleteErr != nil {
return nil, a.deleteErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
Region: proto.String("region"),
},
}, nil
}
type stubInstanceTemplateAPI struct {
deleteErr error
insertErr error
}
func (a stubInstanceTemplateAPI) Close() error {
return nil
}
func (a stubInstanceTemplateAPI) Delete(ctx context.Context, req *computepb.DeleteInstanceTemplateRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.deleteErr != nil {
return nil, a.deleteErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
},
}, nil
}
func (a stubInstanceTemplateAPI) Insert(ctx context.Context, req *computepb.InsertInstanceTemplateRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.insertErr != nil {
return nil, a.insertErr
}
return &stubOperation{
&computepb.Operation{
Name: proto.String("name"),
},
}, nil
}
type stubInstanceGroupManagersAPI struct {
listIterator *stubManagedInstanceIterator
deleteErr error
insertErr error
}
func (a stubInstanceGroupManagersAPI) Close() error {
return nil
}
func (a stubInstanceGroupManagersAPI) Delete(ctx context.Context, req *computepb.DeleteInstanceGroupManagerRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.deleteErr != nil {
return nil, a.deleteErr
}
return &stubOperation{
&computepb.Operation{
Zone: proto.String("zone"),
Name: proto.String("name"),
},
}, nil
}
func (a stubInstanceGroupManagersAPI) Insert(ctx context.Context, req *computepb.InsertInstanceGroupManagerRequest,
opts ...gax.CallOption,
) (Operation, error) {
if a.insertErr != nil {
return nil, a.insertErr
}
return &stubOperation{
&computepb.Operation{
Zone: proto.String("zone"),
Name: proto.String("name"),
},
}, nil
}
func (a stubInstanceGroupManagersAPI) ListManagedInstances(ctx context.Context, req *computepb.ListManagedInstancesInstanceGroupManagersRequest,
opts ...gax.CallOption,
) ManagedInstanceIterator {
return a.listIterator
}
type stubIAMAPI struct {
serviceAccountKeyData []byte
createErr error
createKeyErr error
deleteServiceAccountErr error
}
func (a stubIAMAPI) Close() error {
return nil
}
func (a stubIAMAPI) CreateServiceAccount(ctx context.Context, req *adminpb.CreateServiceAccountRequest, opts ...gax.CallOption) (*adminpb.ServiceAccount, error) {
if a.createErr != nil {
return nil, a.createErr
}
return &adminpb.ServiceAccount{
Name: "name",
ProjectId: "project-id",
UniqueId: "unique-id",
Email: "email",
DisplayName: "display-name",
Description: "description",
Oauth2ClientId: "oauth2-client-id",
Disabled: false,
}, nil
}
func (a stubIAMAPI) CreateServiceAccountKey(ctx context.Context, req *adminpb.CreateServiceAccountKeyRequest, opts ...gax.CallOption) (*adminpb.ServiceAccountKey, error) {
if a.createKeyErr != nil {
return nil, a.createKeyErr
}
return &adminpb.ServiceAccountKey{
Name: "name",
PrivateKeyType: adminpb.ServiceAccountPrivateKeyType_TYPE_GOOGLE_CREDENTIALS_FILE,
KeyAlgorithm: adminpb.ServiceAccountKeyAlgorithm_KEY_ALG_RSA_2048,
PrivateKeyData: a.serviceAccountKeyData,
PublicKeyData: []byte("public-key-data"),
ValidAfterTime: timestamppb.New(time.Time{}),
ValidBeforeTime: timestamppb.New(time.Time{}),
KeyOrigin: adminpb.ServiceAccountKeyOrigin_GOOGLE_PROVIDED,
KeyType: adminpb.ListServiceAccountKeysRequest_USER_MANAGED,
}, nil
}
func (a stubIAMAPI) DeleteServiceAccount(ctx context.Context, req *adminpb.DeleteServiceAccountRequest, opts ...gax.CallOption) error {
return a.deleteServiceAccountErr
}
type stubProjectsAPI struct {
getPolicyErr error
setPolicyErr error
}
func (a stubProjectsAPI) Close() error {
return nil
}
func (a stubProjectsAPI) GetIamPolicy(ctx context.Context, req *iampb.GetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
if a.getPolicyErr != nil {
return nil, a.getPolicyErr
}
return &iampb.Policy{
Version: 3,
Bindings: []*iampb.Binding{
{
Role: "role",
Members: []string{
"member",
},
},
},
Etag: []byte("etag"),
}, nil
}
func (a stubProjectsAPI) SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
if a.setPolicyErr != nil {
return nil, a.setPolicyErr
}
return &iampb.Policy{
Version: 3,
Bindings: []*iampb.Binding{
{
Role: "role",
Members: []string{
"member",
},
},
},
Etag: []byte("etag"),
}, nil
}
type stubManagedInstanceIterator struct {
instances []*computepb.ManagedInstance
nextErr error
internalCounter int
}
func (i *stubManagedInstanceIterator) Next() (*computepb.ManagedInstance, error) {
if i.nextErr != nil {
return nil, i.nextErr
}
if i.internalCounter >= len(i.instances) {
i.internalCounter = 0
return nil, iterator.Done
}
resp := i.instances[i.internalCounter]
i.internalCounter++
return resp, nil
}

View file

@ -0,0 +1,384 @@
package client
import (
"context"
"crypto/rand"
"errors"
"fmt"
"math/big"
"strings"
compute "cloud.google.com/go/compute/apiv1"
admin "cloud.google.com/go/iam/admin/apiv1"
resourcemanager "cloud.google.com/go/resourcemanager/apiv3"
"github.com/edgelesssys/constellation/cli/cloud/cloudtypes"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/state"
)
// Client is a client for the Google Compute Engine.
type Client struct {
instanceAPI
operationRegionAPI
operationZoneAPI
operationGlobalAPI
networksAPI
subnetworksAPI
firewallsAPI
instanceTemplateAPI
instanceGroupManagersAPI
iamAPI
projectsAPI
nodes cloudtypes.Instances
coordinators cloudtypes.Instances
nodesInstanceGroup string
coordinatorInstanceGroup string
coordinatorTemplate string
nodeTemplate string
network string
subnetwork string
secondarySubnetworkRange string
firewalls []string
name string
project string
uid string
zone string
region string
serviceAccount string
}
// NewFromDefault creates an uninitialized client.
func NewFromDefault(ctx context.Context) (*Client, error) {
var closers []closer
insAPI, err := compute.NewInstancesRESTClient(ctx)
if err != nil {
return nil, err
}
closers = append(closers, insAPI)
opZoneAPI, err := compute.NewZoneOperationsRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, opZoneAPI)
opRegionAPI, err := compute.NewRegionOperationsRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, opRegionAPI)
opGlobalAPI, err := compute.NewGlobalOperationsRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, opGlobalAPI)
netAPI, err := compute.NewNetworksRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, netAPI)
subnetAPI, err := compute.NewSubnetworksRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, subnetAPI)
fwAPI, err := compute.NewFirewallsRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, fwAPI)
templAPI, err := compute.NewInstanceTemplatesRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, templAPI)
groupAPI, err := compute.NewInstanceGroupManagersRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, groupAPI)
iamAPI, err := admin.NewIamClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, iamAPI)
projectsAPI, err := resourcemanager.NewProjectsClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
return &Client{
instanceAPI: &instanceClient{insAPI},
operationRegionAPI: opRegionAPI,
operationZoneAPI: opZoneAPI,
operationGlobalAPI: opGlobalAPI,
networksAPI: &networksClient{netAPI},
subnetworksAPI: &subnetworksClient{subnetAPI},
firewallsAPI: &firewallsClient{fwAPI},
instanceTemplateAPI: &instanceTemplateClient{templAPI},
instanceGroupManagersAPI: &instanceGroupManagersClient{groupAPI},
iamAPI: &iamClient{iamAPI},
projectsAPI: &projectsClient{projectsAPI},
nodes: make(cloudtypes.Instances),
coordinators: make(cloudtypes.Instances),
}, nil
}
// NewInitialized creates an initialized client.
func NewInitialized(ctx context.Context, project, zone, region, name string) (*Client, error) {
client, err := NewFromDefault(ctx)
if err != nil {
return nil, err
}
err = client.init(project, zone, region, name)
return client, err
}
// Close closes the client's connection.
func (c *Client) Close() error {
closers := []closer{
c.instanceAPI,
c.operationZoneAPI,
c.operationGlobalAPI,
c.networksAPI,
c.firewallsAPI,
c.instanceTemplateAPI,
c.instanceGroupManagersAPI,
}
return closeAll(closers)
}
// init initializes the client.
func (c *Client) init(project, zone, region, name string) error {
c.project = project
c.zone = zone
c.name = name
c.region = region
uid, err := c.generateUID()
if err != nil {
return err
}
c.uid = uid
return nil
}
// GetState returns the state of the client as ConstellationState.
func (c *Client) GetState() (state.ConstellationState, error) {
var stat state.ConstellationState
stat.CloudProvider = cloudprovider.GCP.String()
if len(c.nodes) == 0 {
return state.ConstellationState{}, errors.New("client has no nodes")
}
stat.GCPNodes = c.nodes
if len(c.coordinators) == 0 {
return state.ConstellationState{}, errors.New("client has no coordinators")
}
stat.GCPCoordinators = c.coordinators
if c.nodesInstanceGroup == "" {
return state.ConstellationState{}, errors.New("client has no nodeInstanceGroup")
}
stat.GCPNodeInstanceGroup = c.nodesInstanceGroup
if c.coordinatorInstanceGroup == "" {
return state.ConstellationState{}, errors.New("client has no coordinatorInstanceGroup")
}
stat.GCPCoordinatorInstanceGroup = c.coordinatorInstanceGroup
if c.project == "" {
return state.ConstellationState{}, errors.New("client has no project")
}
stat.GCPProject = c.project
if c.zone == "" {
return state.ConstellationState{}, errors.New("client has no zone")
}
stat.GCPZone = c.zone
if c.region == "" {
return state.ConstellationState{}, errors.New("client has no region")
}
stat.GCPRegion = c.region
if c.name == "" {
return state.ConstellationState{}, errors.New("client has no name")
}
stat.Name = c.name
if c.uid == "" {
return state.ConstellationState{}, errors.New("client has no uid")
}
stat.UID = c.uid
if len(c.firewalls) == 0 {
return state.ConstellationState{}, errors.New("client has no firewalls")
}
stat.GCPFirewalls = c.firewalls
if c.network == "" {
return state.ConstellationState{}, errors.New("client has no network")
}
stat.GCPNetwork = c.network
if c.subnetwork == "" {
return state.ConstellationState{}, errors.New("client has no subnetwork")
}
stat.GCPSubnetwork = c.subnetwork
if c.nodeTemplate == "" {
return state.ConstellationState{}, errors.New("client has no node instance template")
}
stat.GCPNodeInstanceTemplate = c.nodeTemplate
if c.coordinatorTemplate == "" {
return state.ConstellationState{}, errors.New("client has no coordinator instance template")
}
stat.GCPCoordinatorInstanceTemplate = c.coordinatorTemplate
// service account does not have to be set at all times
stat.GCPServiceAccount = c.serviceAccount
return stat, nil
}
// SetState sets the state of the client to the handed ConstellationState.
func (c *Client) SetState(stat state.ConstellationState) error {
if stat.CloudProvider != cloudprovider.GCP.String() {
return errors.New("state is not gcp state")
}
if len(stat.GCPNodes) == 0 {
return errors.New("state has no nodes")
}
c.nodes = stat.GCPNodes
if len(stat.GCPCoordinators) == 0 {
return errors.New("state has no coordinator")
}
c.coordinators = stat.GCPCoordinators
if stat.GCPNodeInstanceGroup == "" {
return errors.New("state has no nodeInstanceGroup")
}
c.nodesInstanceGroup = stat.GCPNodeInstanceGroup
if stat.GCPCoordinatorInstanceGroup == "" {
return errors.New("state has no coordinatorInstanceGroup")
}
c.coordinatorInstanceGroup = stat.GCPCoordinatorInstanceGroup
if stat.GCPProject == "" {
return errors.New("state has no project")
}
c.project = stat.GCPProject
if stat.GCPZone == "" {
return errors.New("state has no zone")
}
c.zone = stat.GCPZone
if stat.GCPRegion == "" {
return errors.New("state has no region")
}
c.region = stat.GCPRegion
if stat.Name == "" {
return errors.New("state has no name")
}
c.name = stat.Name
if stat.UID == "" {
return errors.New("state has no uid")
}
c.uid = stat.UID
if len(stat.GCPFirewalls) == 0 {
return errors.New("state has no firewalls")
}
c.firewalls = stat.GCPFirewalls
if stat.GCPNetwork == "" {
return errors.New("state has no network")
}
c.network = stat.GCPNetwork
if stat.GCPSubnetwork == "" {
return errors.New("state has no subnetwork")
}
c.subnetwork = stat.GCPSubnetwork
if stat.GCPNodeInstanceTemplate == "" {
return errors.New("state has no node instance template")
}
c.nodeTemplate = stat.GCPNodeInstanceTemplate
if stat.GCPCoordinatorInstanceTemplate == "" {
return errors.New("state has no coordinator instance template")
}
c.coordinatorTemplate = stat.GCPCoordinatorInstanceTemplate
// service account does not have to be set at all times
c.serviceAccount = stat.GCPServiceAccount
return nil
}
func (c *Client) generateUID() (string, error) {
letters := []byte("abcdefghijklmnopqrstuvwxyz0123456789")
const uidLen = 5
uid := make([]byte, uidLen)
for i := 0; i < uidLen; i++ {
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
uid[i] = letters[n.Int64()]
}
return string(uid), nil
}
type closer interface {
Close() error
}
// closeAll closes all closers, even if an error occurs.
//
// Errors are collected and a composed error is returned.
func closeAll(closers []closer) error {
// Since this function is intended to be deferred, it will always call all
// close operations, even if a previous operation failed. The if multiple
// errors occur, the returned error will be composed of the error messages
// of those errors.
var errs []error
for _, closer := range closers {
errs = append(errs, closer.Close())
}
return composeErr(errs)
}
// composeErr composes a list of errors to a single error.
//
// If all errs are nil, the returned error is also nil.
func composeErr(errs []error) error {
var composed strings.Builder
for i, err := range errs {
if err != nil {
composed.WriteString(fmt.Sprintf("%d: %s", i, err.Error()))
}
}
if composed.Len() != 0 {
return errors.New(composed.String())
}
return nil
}

View file

@ -0,0 +1,684 @@
package client
import (
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloud/cloudtypes"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/state"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSetGetState(t *testing.T) {
testCases := map[string]struct {
state state.ConstellationState
wantErr bool
}{
"valid state": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
GCPServiceAccount: "service-account",
},
},
"missing nodes": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing coordinator": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing node group": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing coordinator group": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing project id": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing zone": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing region": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing name": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
UID: "uid",
GCPRegion: "region-id",
GCPNetwork: "net-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing uid": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
Name: "name",
GCPRegion: "region-id",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing firewalls": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing network": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing external network": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing subnetwork": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing external subnetwork": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing node template": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPCoordinatorInstanceTemplate: "temp-id",
},
wantErr: true,
},
"missing coordinator template": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
},
wantErr: true,
},
}
t.Run("SetState", func(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
client := Client{}
if tc.wantErr {
assert.Error(client.SetState(tc.state))
} else {
assert.NoError(client.SetState(tc.state))
assert.Equal(tc.state.GCPNodes, client.nodes)
assert.Equal(tc.state.GCPCoordinators, client.coordinators)
assert.Equal(tc.state.GCPNodeInstanceGroup, client.nodesInstanceGroup)
assert.Equal(tc.state.GCPCoordinatorInstanceGroup, client.coordinatorInstanceGroup)
assert.Equal(tc.state.GCPProject, client.project)
assert.Equal(tc.state.GCPZone, client.zone)
assert.Equal(tc.state.Name, client.name)
assert.Equal(tc.state.UID, client.uid)
assert.Equal(tc.state.GCPNetwork, client.network)
assert.Equal(tc.state.GCPFirewalls, client.firewalls)
assert.Equal(tc.state.GCPCoordinatorInstanceTemplate, client.coordinatorTemplate)
assert.Equal(tc.state.GCPNodeInstanceTemplate, client.nodeTemplate)
assert.Equal(tc.state.GCPServiceAccount, client.serviceAccount)
}
})
}
})
t.Run("GetState", func(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
client := Client{
nodes: tc.state.GCPNodes,
coordinators: tc.state.GCPCoordinators,
nodesInstanceGroup: tc.state.GCPNodeInstanceGroup,
coordinatorInstanceGroup: tc.state.GCPCoordinatorInstanceGroup,
project: tc.state.GCPProject,
zone: tc.state.GCPZone,
region: tc.state.GCPRegion,
name: tc.state.Name,
uid: tc.state.UID,
network: tc.state.GCPNetwork,
subnetwork: tc.state.GCPSubnetwork,
firewalls: tc.state.GCPFirewalls,
nodeTemplate: tc.state.GCPNodeInstanceTemplate,
coordinatorTemplate: tc.state.GCPCoordinatorInstanceTemplate,
serviceAccount: tc.state.GCPServiceAccount,
}
if tc.wantErr {
_, err := client.GetState()
assert.Error(err)
} else {
stat, err := client.GetState()
assert.NoError(err)
assert.Equal(tc.state, stat)
}
})
}
})
}
func TestSetStateCloudProvider(t *testing.T) {
assert := assert.New(t)
client := Client{}
stateMissingCloudProvider := state.ConstellationState{
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
}
assert.Error(client.SetState(stateMissingCloudProvider))
stateIncorrectCloudProvider := state.ConstellationState{
CloudProvider: "incorrect",
GCPNodes: cloudtypes.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: cloudtypes.Instances{
"id-1": {
PublicIP: "ip3",
PrivateIP: "ip4",
},
},
GCPNodeInstanceGroup: "group-id",
GCPCoordinatorInstanceGroup: "group-id",
GCPProject: "proj-id",
GCPZone: "zone-id",
GCPRegion: "region-id",
Name: "name",
UID: "uid",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPNodeInstanceTemplate: "temp-id",
GCPCoordinatorInstanceTemplate: "temp-id",
}
assert.Error(client.SetState(stateIncorrectCloudProvider))
}
func TestInit(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{}
require.NoError(client.init("project", "zone", "region", "name"))
assert.Equal("project", client.project)
assert.Equal("zone", client.zone)
assert.Equal("region", client.region)
assert.Equal("name", client.name)
}
func TestCloseAll(t *testing.T) {
assert := assert.New(t)
closers := []closer{&someCloser{}, &someCloser{}, &someCloser{}}
assert.NoError(closeAll(closers))
for _, c := range closers {
assert.True(c.(*someCloser).closed)
}
someErr := errors.New("failed")
closers = []closer{&someCloser{}, &someCloser{closeErr: someErr}, &someCloser{}}
assert.Error(closeAll(closers))
for _, c := range closers {
assert.True(c.(*someCloser).closed)
}
}
type someCloser struct {
closeErr error
closed bool
}
func (c *someCloser) Close() error {
c.closed = true
return c.closeErr
}
func TestComposedErr(t *testing.T) {
assert := assert.New(t)
noErrs := []error{nil, nil, nil}
assert.NoError(composeErr(noErrs))
someErrs := []error{
errors.New("failed 4"),
errors.New("failed 7"),
nil,
nil,
errors.New("failed 9"),
}
err := composeErr(someErrs)
assert.Error(err)
assert.Contains(err.Error(), "4")
assert.Contains(err.Error(), "7")
assert.Contains(err.Error(), "9")
}

View file

@ -0,0 +1,169 @@
package client
import (
"context"
compute "cloud.google.com/go/compute/apiv1"
admin "cloud.google.com/go/iam/admin/apiv1"
resourcemanager "cloud.google.com/go/resourcemanager/apiv3"
"github.com/googleapis/gax-go/v2"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
adminpb "google.golang.org/genproto/googleapis/iam/admin/v1"
iampb "google.golang.org/genproto/googleapis/iam/v1"
)
type instanceClient struct {
*compute.InstancesClient
}
func (c *instanceClient) Close() error {
return c.InstancesClient.Close()
}
func (c *instanceClient) List(ctx context.Context, req *computepb.ListInstancesRequest,
opts ...gax.CallOption,
) InstanceIterator {
return c.InstancesClient.List(ctx, req)
}
type firewallsClient struct {
*compute.FirewallsClient
}
func (c *firewallsClient) Close() error {
return c.FirewallsClient.Close()
}
func (c *firewallsClient) Delete(ctx context.Context, req *computepb.DeleteFirewallRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.FirewallsClient.Delete(ctx, req)
}
func (c *firewallsClient) Insert(ctx context.Context, req *computepb.InsertFirewallRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.FirewallsClient.Insert(ctx, req)
}
type networksClient struct {
*compute.NetworksClient
}
func (c *networksClient) Close() error {
return c.NetworksClient.Close()
}
func (c *networksClient) Insert(ctx context.Context, req *computepb.InsertNetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.NetworksClient.Insert(ctx, req)
}
func (c *networksClient) Delete(ctx context.Context, req *computepb.DeleteNetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.NetworksClient.Delete(ctx, req)
}
type subnetworksClient struct {
*compute.SubnetworksClient
}
func (c *subnetworksClient) Close() error {
return c.SubnetworksClient.Close()
}
func (c *subnetworksClient) Insert(ctx context.Context, req *computepb.InsertSubnetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.SubnetworksClient.Insert(ctx, req)
}
func (c *subnetworksClient) Delete(ctx context.Context, req *computepb.DeleteSubnetworkRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.SubnetworksClient.Delete(ctx, req)
}
type instanceTemplateClient struct {
*compute.InstanceTemplatesClient
}
func (c *instanceTemplateClient) Close() error {
return c.InstanceTemplatesClient.Close()
}
func (c *instanceTemplateClient) Delete(ctx context.Context, req *computepb.DeleteInstanceTemplateRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.InstanceTemplatesClient.Delete(ctx, req)
}
func (c *instanceTemplateClient) Insert(ctx context.Context, req *computepb.InsertInstanceTemplateRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.InstanceTemplatesClient.Insert(ctx, req)
}
type instanceGroupManagersClient struct {
*compute.InstanceGroupManagersClient
}
func (c *instanceGroupManagersClient) Close() error {
return c.InstanceGroupManagersClient.Close()
}
func (c *instanceGroupManagersClient) Delete(ctx context.Context, req *computepb.DeleteInstanceGroupManagerRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.InstanceGroupManagersClient.Delete(ctx, req)
}
func (c *instanceGroupManagersClient) Insert(ctx context.Context, req *computepb.InsertInstanceGroupManagerRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.InstanceGroupManagersClient.Insert(ctx, req)
}
func (c *instanceGroupManagersClient) ListManagedInstances(ctx context.Context, req *computepb.ListManagedInstancesInstanceGroupManagersRequest,
opts ...gax.CallOption,
) ManagedInstanceIterator {
return c.InstanceGroupManagersClient.ListManagedInstances(ctx, req)
}
type iamClient struct {
*admin.IamClient
}
func (c *iamClient) Close() error {
return c.IamClient.Close()
}
func (c *iamClient) CreateServiceAccount(ctx context.Context, req *adminpb.CreateServiceAccountRequest, opts ...gax.CallOption) (*adminpb.ServiceAccount, error) {
return c.IamClient.CreateServiceAccount(ctx, req)
}
func (c *iamClient) CreateServiceAccountKey(ctx context.Context, req *adminpb.CreateServiceAccountKeyRequest, opts ...gax.CallOption) (*adminpb.ServiceAccountKey, error) {
return c.IamClient.CreateServiceAccountKey(ctx, req)
}
func (c *iamClient) DeleteServiceAccount(ctx context.Context, req *adminpb.DeleteServiceAccountRequest, opts ...gax.CallOption) error {
return c.IamClient.DeleteServiceAccount(ctx, req)
}
type projectsClient struct {
*resourcemanager.ProjectsClient
}
func (c *projectsClient) Close() error {
return c.ProjectsClient.Close()
}
func (c *projectsClient) GetIamPolicy(ctx context.Context, req *iampb.GetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
return c.ProjectsClient.GetIamPolicy(ctx, req)
}
func (c *projectsClient) SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest, opts ...gax.CallOption) (*iampb.Policy, error) {
return c.ProjectsClient.SetIamPolicy(ctx, req)
}

View file

@ -0,0 +1,413 @@
package client
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/edgelesssys/constellation/cli/cloud/cloudtypes"
"github.com/edgelesssys/constellation/coordinator/role"
"google.golang.org/api/iterator"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
// CreateInstances creates instances (virtual machines) on Google Compute Engine.
//
// A separate managed instance group is created for coordinators and nodes, the function
// waits until the instances are up and stores the public and private IPs of the instances
// in the client. If the client's network must be set before instances can be created.
func (c *Client) CreateInstances(ctx context.Context, input CreateInstancesInput) error {
if c.network == "" {
return errors.New("client has no network")
}
ops := []Operation{}
nodeTemplateInput := insertInstanceTemplateInput{
Name: c.name + "-worker-" + c.uid,
Network: c.network,
SecondarySubnetworkRangeName: c.secondarySubnetworkRange,
Subnetwork: c.subnetwork,
ImageId: input.ImageId,
InstanceType: input.InstanceType,
StateDiskSizeGB: int64(input.StateDiskSizeGB),
Role: role.Node.String(),
KubeEnv: input.KubeEnv,
Project: c.project,
Zone: c.zone,
Region: c.region,
UID: c.uid,
}
op, err := c.insertInstanceTemplate(ctx, nodeTemplateInput)
if err != nil {
return fmt.Errorf("inserting instanceTemplate failed: %w", err)
}
ops = append(ops, op)
c.nodeTemplate = nodeTemplateInput.Name
coordinatorTemplateInput := insertInstanceTemplateInput{
Name: c.name + "-control-plane-" + c.uid,
Network: c.network,
Subnetwork: c.subnetwork,
SecondarySubnetworkRangeName: c.secondarySubnetworkRange,
ImageId: input.ImageId,
InstanceType: input.InstanceType,
StateDiskSizeGB: int64(input.StateDiskSizeGB),
Role: role.Coordinator.String(),
KubeEnv: input.KubeEnv,
Project: c.project,
Zone: c.zone,
Region: c.region,
UID: c.uid,
}
op, err = c.insertInstanceTemplate(ctx, coordinatorTemplateInput)
if err != nil {
return fmt.Errorf("inserting instanceTemplate failed: %w", err)
}
ops = append(ops, op)
c.coordinatorTemplate = coordinatorTemplateInput.Name
if err := c.waitForOperations(ctx, ops); err != nil {
return err
}
ops = []Operation{}
coordinatorGroupInput := instanceGroupManagerInput{
Count: input.CountCoordinators,
Name: strings.Join([]string{c.name, "control-plane", c.uid}, "-"),
Template: c.coordinatorTemplate,
UID: c.uid,
Project: c.project,
Zone: c.zone,
}
op, err = c.insertInstanceGroupManger(ctx, coordinatorGroupInput)
if err != nil {
return fmt.Errorf("inserting instanceGroupManager failed: %w", err)
}
ops = append(ops, op)
c.coordinatorInstanceGroup = coordinatorGroupInput.Name
nodeGroupInput := instanceGroupManagerInput{
Count: input.CountNodes,
Name: strings.Join([]string{c.name, "worker", c.uid}, "-"),
Template: c.nodeTemplate,
UID: c.uid,
Project: c.project,
Zone: c.zone,
}
op, err = c.insertInstanceGroupManger(ctx, nodeGroupInput)
if err != nil {
return fmt.Errorf("inserting instanceGroupManager failed: %w", err)
}
ops = append(ops, op)
c.nodesInstanceGroup = nodeGroupInput.Name
if err := c.waitForOperations(ctx, ops); err != nil {
return err
}
if err := c.waitForInstanceGroupScaling(ctx, c.nodesInstanceGroup); err != nil {
return fmt.Errorf("waiting for instanceGroupScaling failed: %w", err)
}
if err := c.waitForInstanceGroupScaling(ctx, c.coordinatorInstanceGroup); err != nil {
return fmt.Errorf("waiting for instanceGroupScaling failed: %w", err)
}
if err := c.getInstanceIPs(ctx, c.nodesInstanceGroup, c.nodes); err != nil {
return fmt.Errorf("failed to get instanceIPs: %w", err)
}
if err := c.getInstanceIPs(ctx, c.coordinatorInstanceGroup, c.coordinators); err != nil {
return fmt.Errorf("failed to get instanceIPs: %w", err)
}
return nil
}
// TerminateInstances terminates the clients instances.
func (c *Client) TerminateInstances(ctx context.Context) error {
ops := []Operation{}
if c.nodesInstanceGroup != "" {
op, err := c.deleteInstanceGroupManager(ctx, c.nodesInstanceGroup)
if err != nil {
return fmt.Errorf("deleting instanceGroupManager '%s' failed: %w", c.nodesInstanceGroup, err)
}
ops = append(ops, op)
c.nodesInstanceGroup = ""
c.nodes = make(cloudtypes.Instances)
}
if c.coordinatorInstanceGroup != "" {
op, err := c.deleteInstanceGroupManager(ctx, c.coordinatorInstanceGroup)
if err != nil {
return fmt.Errorf("deleting instanceGroupManager '%s' failed: %w", c.coordinatorInstanceGroup, err)
}
ops = append(ops, op)
c.coordinatorInstanceGroup = ""
c.coordinators = make(cloudtypes.Instances)
}
if err := c.waitForOperations(ctx, ops); err != nil {
return err
}
ops = []Operation{}
if c.nodeTemplate != "" {
op, err := c.deleteInstanceTemplate(ctx, c.nodeTemplate)
if err != nil {
return fmt.Errorf("deleting instanceTemplate failed: %w", err)
}
ops = append(ops, op)
c.nodeTemplate = ""
}
if c.coordinatorTemplate != "" {
op, err := c.deleteInstanceTemplate(ctx, c.coordinatorTemplate)
if err != nil {
return fmt.Errorf("deleting instanceTemplate failed: %w", err)
}
ops = append(ops, op)
c.coordinatorTemplate = ""
}
return c.waitForOperations(ctx, ops)
}
func (c *Client) insertInstanceTemplate(ctx context.Context, input insertInstanceTemplateInput) (Operation, error) {
req := input.insertInstanceTemplateRequest()
return c.instanceTemplateAPI.Insert(ctx, req)
}
func (c *Client) deleteInstanceTemplate(ctx context.Context, name string) (Operation, error) {
req := &computepb.DeleteInstanceTemplateRequest{
InstanceTemplate: name,
Project: c.project,
}
return c.instanceTemplateAPI.Delete(ctx, req)
}
func (c *Client) insertInstanceGroupManger(ctx context.Context, input instanceGroupManagerInput) (Operation, error) {
req := input.InsertInstanceGroupManagerRequest()
return c.instanceGroupManagersAPI.Insert(ctx, &req)
}
func (c *Client) deleteInstanceGroupManager(ctx context.Context, instanceGroupManagerName string) (Operation, error) {
req := &computepb.DeleteInstanceGroupManagerRequest{
InstanceGroupManager: instanceGroupManagerName,
Project: c.project,
Zone: c.zone,
}
return c.instanceGroupManagersAPI.Delete(ctx, req)
}
func (c *Client) waitForInstanceGroupScaling(ctx context.Context, groupId string) error {
for {
if err := ctx.Err(); err != nil {
return err
}
listReq := &computepb.ListManagedInstancesInstanceGroupManagersRequest{
InstanceGroupManager: groupId,
Project: c.project,
Zone: c.zone,
}
it := c.instanceGroupManagersAPI.ListManagedInstances(ctx, listReq)
for {
resp, err := it.Next()
if errors.Is(err, iterator.Done) {
return nil
}
if err != nil {
return err
}
if resp.CurrentAction == nil {
return errors.New("currentAction is nil")
}
if *resp.CurrentAction != computepb.ManagedInstance_NONE.String() {
time.Sleep(5 * time.Second)
break
}
}
}
}
// getInstanceIPs requests the IPs of the client's instances.
func (c *Client) getInstanceIPs(ctx context.Context, groupId string, list cloudtypes.Instances) error {
req := &computepb.ListInstancesRequest{
Filter: proto.String("name=" + groupId + "*"),
Project: c.project,
Zone: c.zone,
}
it := c.instanceAPI.List(ctx, req)
for {
resp, err := it.Next()
if errors.Is(err, iterator.Done) {
return nil
}
if err != nil {
return err
}
if resp.Name == nil {
return errors.New("instance name is nil pointer")
}
if len(resp.NetworkInterfaces) == 0 {
return errors.New("network interface is empty")
}
if resp.NetworkInterfaces[0].NetworkIP == nil {
return errors.New("networkIP is nil")
}
if len(resp.NetworkInterfaces[0].AccessConfigs) == 0 {
return errors.New("access configs is empty")
}
if resp.NetworkInterfaces[0].AccessConfigs[0].NatIP == nil {
return errors.New("natIP is nil")
}
instance := cloudtypes.Instance{
PrivateIP: *resp.NetworkInterfaces[0].NetworkIP,
PublicIP: *resp.NetworkInterfaces[0].AccessConfigs[0].NatIP,
}
list[*resp.Name] = instance
}
}
type instanceGroupManagerInput struct {
Count int
Name string
Template string
Project string
Zone string
UID string
}
func (i *instanceGroupManagerInput) InsertInstanceGroupManagerRequest() computepb.InsertInstanceGroupManagerRequest {
return computepb.InsertInstanceGroupManagerRequest{
InstanceGroupManagerResource: &computepb.InstanceGroupManager{
BaseInstanceName: proto.String(i.Name),
InstanceTemplate: proto.String("projects/" + i.Project + "/global/instanceTemplates/" + i.Template),
Name: proto.String(i.Name),
TargetSize: proto.Int32(int32(i.Count)),
},
Project: i.Project,
Zone: i.Zone,
}
}
// CreateInstancesInput is the input for a CreatInstances operation.
type CreateInstancesInput struct {
CountNodes int
CountCoordinators int
ImageId string
InstanceType string
StateDiskSizeGB int
KubeEnv string
}
type insertInstanceTemplateInput struct {
Name string
Network string
Subnetwork string
SecondarySubnetworkRangeName string
ImageId string
InstanceType string
StateDiskSizeGB int64
Role string
KubeEnv string
Project string
Zone string
Region string
UID string
}
func (i insertInstanceTemplateInput) insertInstanceTemplateRequest() *computepb.InsertInstanceTemplateRequest {
req := computepb.InsertInstanceTemplateRequest{
InstanceTemplateResource: &computepb.InstanceTemplate{
Description: proto.String("This instance belongs to a Constellation cluster."),
Name: proto.String(i.Name),
Properties: &computepb.InstanceProperties{
ConfidentialInstanceConfig: &computepb.ConfidentialInstanceConfig{
EnableConfidentialCompute: proto.Bool(true),
},
Description: proto.String("This instance belongs to a Constellation cluster."),
Disks: []*computepb.AttachedDisk{
{
InitializeParams: &computepb.AttachedDiskInitializeParams{
DiskSizeGb: proto.Int64(10),
SourceImage: proto.String(i.ImageId),
},
AutoDelete: proto.Bool(true),
Boot: proto.Bool(true),
Mode: proto.String(computepb.AttachedDisk_READ_WRITE.String()),
},
{
InitializeParams: &computepb.AttachedDiskInitializeParams{
DiskSizeGb: proto.Int64(i.StateDiskSizeGB),
},
AutoDelete: proto.Bool(true),
DeviceName: proto.String("state-disk"),
Mode: proto.String(computepb.AttachedDisk_READ_WRITE.String()),
Type: proto.String(computepb.AttachedDisk_PERSISTENT.String()),
},
},
MachineType: proto.String(i.InstanceType),
Metadata: &computepb.Metadata{
Items: []*computepb.Items{
{
Key: proto.String("kube-env"),
Value: proto.String(i.KubeEnv),
},
{
Key: proto.String("constellation-uid"),
Value: proto.String(i.UID),
},
{
Key: proto.String("constellation-role"),
Value: proto.String(i.Role),
},
},
},
NetworkInterfaces: []*computepb.NetworkInterface{
{
Network: proto.String("projects/" + i.Project + "/global/networks/" + i.Network),
Subnetwork: proto.String("regions/" + i.Region + "/subnetworks/" + i.Subnetwork),
AccessConfigs: []*computepb.AccessConfig{
{Type: proto.String(computepb.AccessConfig_ONE_TO_ONE_NAT.String())},
},
},
},
Scheduling: &computepb.Scheduling{
OnHostMaintenance: proto.String(computepb.Scheduling_TERMINATE.String()),
},
ServiceAccounts: []*computepb.ServiceAccount{
{
Scopes: []string{
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/servicecontrol",
"https://www.googleapis.com/auth/service.management.readonly",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring.write",
"https://www.googleapis.com/auth/trace.append",
},
},
},
ShieldedInstanceConfig: &computepb.ShieldedInstanceConfig{
EnableIntegrityMonitoring: proto.Bool(true),
EnableSecureBoot: proto.Bool(true),
EnableVtpm: proto.Bool(true),
},
Tags: &computepb.Tags{
Items: []string{"constellation-" + i.UID},
},
},
},
Project: i.Project,
}
// if there is an secondary IP range defined, we use it as an alias IP range
if i.SecondarySubnetworkRangeName != "" {
req.InstanceTemplateResource.Properties.NetworkInterfaces[0].AliasIpRanges = []*computepb.AliasIpRange{
{
IpCidrRange: proto.String("/24"),
SubnetworkRangeName: proto.String(i.SecondarySubnetworkRangeName),
},
}
}
return &req
}

View file

@ -0,0 +1,263 @@
package client
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloud/cloudtypes"
"github.com/stretchr/testify/assert"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
func TestCreateInstances(t *testing.T) {
testInstances := []*computepb.Instance{
{
Name: proto.String("instance-name-1"),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{NatIP: proto.String("public-ip")},
},
NetworkIP: proto.String("private-ip"),
},
},
},
{
Name: proto.String("instance-name-2"),
NetworkInterfaces: []*computepb.NetworkInterface{
{
AccessConfigs: []*computepb.AccessConfig{
{NatIP: proto.String("public-ip")},
},
NetworkIP: proto.String("private-ip"),
},
},
},
}
testManagedInstances := []*computepb.ManagedInstance{
{CurrentAction: proto.String(computepb.ManagedInstance_NONE.String())},
{CurrentAction: proto.String(computepb.ManagedInstance_NONE.String())},
}
testInput := CreateInstancesInput{
CountCoordinators: 3,
CountNodes: 4,
ImageId: "img",
InstanceType: "n2d-standard-2",
KubeEnv: "kube-env",
}
someErr := errors.New("failed")
testCases := map[string]struct {
instanceAPI instanceAPI
operationZoneAPI operationZoneAPI
operationGlobalAPI operationGlobalAPI
instanceTemplateAPI instanceTemplateAPI
instanceGroupManagersAPI instanceGroupManagersAPI
input CreateInstancesInput
network string
wantErr bool
}{
"successful create": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{listIterator: &stubManagedInstanceIterator{instances: testManagedInstances}},
network: "network",
input: testInput,
},
"failed no network": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{waitErr: someErr},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{listIterator: &stubManagedInstanceIterator{instances: testManagedInstances}},
input: testInput,
wantErr: true,
},
"failed wait zonal op": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{waitErr: someErr},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{listIterator: &stubManagedInstanceIterator{instances: testManagedInstances}},
network: "network",
input: testInput,
wantErr: true,
},
"failed wait global op": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{waitErr: someErr},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{listIterator: &stubManagedInstanceIterator{instances: testManagedInstances}},
network: "network",
input: testInput,
wantErr: true,
},
"failed insert template": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{insertErr: someErr},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{listIterator: &stubManagedInstanceIterator{instances: testManagedInstances}},
input: testInput,
network: "network",
wantErr: true,
},
"failed insert instanceGroupManager": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{insertErr: someErr},
network: "network",
input: testInput,
wantErr: true,
},
"failed instanceGroupManager iterator": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{listIterator: &stubManagedInstanceIterator{nextErr: someErr}},
network: "network",
input: testInput,
wantErr: true,
},
"failed instance iterator": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{nextErr: someErr}},
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{listIterator: &stubManagedInstanceIterator{instances: testManagedInstances}},
network: "network",
input: testInput,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
network: tc.network,
subnetwork: "subnetwork",
secondarySubnetworkRange: "secondary-range",
instanceAPI: tc.instanceAPI,
operationZoneAPI: tc.operationZoneAPI,
operationGlobalAPI: tc.operationGlobalAPI,
instanceTemplateAPI: tc.instanceTemplateAPI,
instanceGroupManagersAPI: tc.instanceGroupManagersAPI,
nodes: make(cloudtypes.Instances),
coordinators: make(cloudtypes.Instances),
}
if tc.wantErr {
assert.Error(client.CreateInstances(ctx, tc.input))
} else {
assert.NoError(client.CreateInstances(ctx, tc.input))
assert.Equal([]string{"public-ip", "public-ip"}, client.nodes.PublicIPs())
assert.Equal([]string{"private-ip", "private-ip"}, client.nodes.PrivateIPs())
assert.Equal([]string{"public-ip", "public-ip"}, client.coordinators.PublicIPs())
assert.Equal([]string{"private-ip", "private-ip"}, client.coordinators.PrivateIPs())
assert.NotNil(client.nodesInstanceGroup)
assert.NotNil(client.coordinatorInstanceGroup)
assert.NotNil(client.coordinatorTemplate)
assert.NotNil(client.nodeTemplate)
}
})
}
}
func TestTerminateInstances(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
operationZoneAPI operationZoneAPI
operationGlobalAPI operationGlobalAPI
instanceTemplateAPI instanceTemplateAPI
instanceGroupManagersAPI instanceGroupManagersAPI
missingNodeInstanceGroup bool
wantErr bool
}{
"successful terminate": {
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{},
},
"successful terminate with missing node instance group": {
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{},
missingNodeInstanceGroup: true,
},
"fail delete instanceGroupManager": {
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{deleteErr: someErr},
wantErr: true,
},
"fail delete instanceTemplate": {
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{deleteErr: someErr},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
operationZoneAPI: tc.operationZoneAPI,
operationGlobalAPI: tc.operationGlobalAPI,
instanceTemplateAPI: tc.instanceTemplateAPI,
instanceGroupManagersAPI: tc.instanceGroupManagersAPI,
nodes: cloudtypes.Instances{"node-id-1": cloudtypes.Instance{}, "node-id-2": cloudtypes.Instance{}},
coordinators: cloudtypes.Instances{"coordinator-id-1": cloudtypes.Instance{}},
firewalls: []string{"firewall-1", "firewall-2"},
network: "network-id-1",
nodesInstanceGroup: "nodeInstanceGroup-id-1",
coordinatorInstanceGroup: "coordinatorInstanceGroup-id-1",
nodeTemplate: "template-id-1",
coordinatorTemplate: "template-id-1",
}
if tc.missingNodeInstanceGroup {
client.nodesInstanceGroup = ""
client.nodes = cloudtypes.Instances{}
}
if tc.wantErr {
assert.Error(client.TerminateInstances(ctx))
} else {
assert.NoError(client.TerminateInstances(ctx))
assert.Nil(client.nodes.PublicIPs())
assert.Nil(client.nodes.PrivateIPs())
assert.Nil(client.coordinators.PublicIPs())
assert.Nil(client.coordinators.PrivateIPs())
assert.Empty(client.nodesInstanceGroup)
assert.Empty(client.coordinatorInstanceGroup)
assert.Empty(client.coordinatorTemplate)
assert.Empty(client.nodeTemplate)
}
})
}
}

View file

@ -0,0 +1,199 @@
package client
import (
"context"
"errors"
"github.com/edgelesssys/constellation/cli/cloud/cloudtypes"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
const (
SubnetCIDR = "192.168.178.0/24"
SubnetExtCIDR = "10.10.0.0/16"
)
// CreateFirewall creates a set of firewall rules for the client's network.
//
// The client must have a VPC network to set firewall rules.
func (c *Client) CreateFirewall(ctx context.Context, input FirewallInput) error {
if c.network == "" {
return errors.New("client has not network")
}
firewallRules, err := input.Ingress.GCP()
if err != nil {
return err
}
var ops []Operation
for _, rule := range firewallRules {
c.firewalls = append(c.firewalls, rule.GetName())
rule.Network = proto.String("global/networks/" + c.network)
rule.Name = proto.String(rule.GetName() + "-" + c.uid)
req := &computepb.InsertFirewallRequest{
FirewallResource: rule,
Project: c.project,
}
resp, err := c.firewallsAPI.Insert(ctx, req)
if err != nil {
return err
}
if resp.Proto().Name == nil {
return errors.New("operation name is nil")
}
ops = append(ops, resp)
}
return c.waitForOperations(ctx, ops)
}
// TerminateFirewall deletes firewall rules from the client's network.
//
// The client must have a VPC network to set firewall rules.
func (c *Client) TerminateFirewall(ctx context.Context) error {
if len(c.firewalls) == 0 {
return nil
}
var ops []Operation
for _, name := range c.firewalls {
ruleName := name + "-" + c.uid
req := &computepb.DeleteFirewallRequest{
Firewall: ruleName,
Project: c.project,
}
resp, err := c.firewallsAPI.Delete(ctx, req)
if err != nil {
return err
}
if resp.Proto().Name == nil {
return errors.New("operation name is nil")
}
ops = append(ops, resp)
}
if err := c.waitForOperations(ctx, ops); err != nil {
return err
}
c.firewalls = []string{}
return nil
}
// FirewallInput defines firewall rules to be set.
type FirewallInput struct {
Ingress cloudtypes.Firewall
Egress cloudtypes.Firewall
}
// CreateVPCs creates all necessary VPC networks.
func (c *Client) CreateVPCs(ctx context.Context) error {
c.network = c.name + "-" + c.uid
op, err := c.createVPC(ctx, c.network)
if err != nil {
return err
}
if err := c.waitForOperations(ctx, []Operation{op}); err != nil {
return err
}
if err := c.createSubnets(ctx); err != nil {
return err
}
return nil
}
// createVPC creates a VPC network.
func (c *Client) createVPC(ctx context.Context, name string) (Operation, error) {
req := &computepb.InsertNetworkRequest{
NetworkResource: &computepb.Network{
AutoCreateSubnetworks: proto.Bool(false),
Description: proto.String("Constellation VPC"),
Name: proto.String(name),
},
Project: c.project,
}
return c.networksAPI.Insert(ctx, req)
}
// TerminateVPCs terminates all VPC networks.
//
// If the any network has firewall rules, these must be terminated first.
func (c *Client) TerminateVPCs(ctx context.Context) error {
if len(c.firewalls) != 0 {
return errors.New("client has firewalls, which must be deleted first")
}
if err := c.terminateSubnet(ctx); err != nil {
return err
}
var op Operation
var err error
if c.network != "" {
op, err = c.terminateVPC(ctx, c.network)
if err != nil {
return err
}
c.network = ""
}
return c.waitForOperations(ctx, []Operation{op})
}
// terminateVPC terminates a VPC network.
//
// If the network has firewall rules, these must be terminated first.
func (c *Client) terminateVPC(ctx context.Context, network string) (Operation, error) {
req := &computepb.DeleteNetworkRequest{
Project: c.project,
Network: network,
}
return c.networksAPI.Delete(ctx, req)
}
func (c *Client) createSubnets(ctx context.Context) error {
c.subnetwork = "node-net-" + c.uid
c.secondarySubnetworkRange = "net-ext" + c.uid
op, err := c.createSubnet(ctx, c.subnetwork, c.network, c.secondarySubnetworkRange)
if err != nil {
return err
}
return c.waitForOperations(ctx, []Operation{op})
}
func (c *Client) createSubnet(ctx context.Context, name, network, secondaryRangeName string) (Operation, error) {
req := &computepb.InsertSubnetworkRequest{
Project: c.project,
Region: c.region,
SubnetworkResource: &computepb.Subnetwork{
IpCidrRange: proto.String(SubnetCIDR),
Name: proto.String(name),
Network: proto.String("projects/" + c.project + "/global/networks/" + network),
SecondaryIpRanges: []*computepb.SubnetworkSecondaryRange{
{
RangeName: proto.String(secondaryRangeName),
IpCidrRange: proto.String(SubnetExtCIDR),
},
},
},
}
return c.subnetworksAPI.Insert(ctx, req)
}
func (c *Client) terminateSubnet(ctx context.Context) error {
if c.subnetwork == "" {
return nil
}
req := &computepb.DeleteSubnetworkRequest{
Project: c.project,
Region: c.region,
Subnetwork: c.subnetwork,
}
op, err := c.subnetworksAPI.Delete(ctx, req)
if err != nil {
return err
}
return c.waitForOperations(ctx, []Operation{op})
}

View file

@ -0,0 +1,309 @@
package client
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloud/cloudtypes"
"github.com/stretchr/testify/assert"
)
func TestCreateVPCs(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
operationGlobalAPI operationGlobalAPI
operationRegionAPI operationRegionAPI
networksAPI networksAPI
subnetworksAPI subnetworksAPI
wantErr bool
}{
"successful create": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
},
"failed wait global op": {
operationGlobalAPI: stubOperationGlobalAPI{waitErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
wantErr: true,
},
"failed wait region op": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{waitErr: someErr},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
wantErr: true,
},
"failed insert networks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{insertErr: someErr},
subnetworksAPI: stubSubnetworksAPI{},
wantErr: true,
},
"failed insert subnetworks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{insertErr: someErr},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
operationGlobalAPI: tc.operationGlobalAPI,
operationRegionAPI: tc.operationRegionAPI,
networksAPI: tc.networksAPI,
subnetworksAPI: tc.subnetworksAPI,
nodes: make(cloudtypes.Instances),
coordinators: make(cloudtypes.Instances),
}
if tc.wantErr {
assert.Error(client.CreateVPCs(ctx))
} else {
assert.NoError(client.CreateVPCs(ctx))
assert.NotNil(client.network)
}
})
}
}
func TestTerminateVPCs(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
operationGlobalAPI operationGlobalAPI
operationRegionAPI operationRegionAPI
networksAPI networksAPI
subnetworksAPI subnetworksAPI
firewalls []string
subnetwork string
wantErr bool
}{
"successful terminate": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
subnetwork: "subnetwork-id-1",
},
"subnetwork empty": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
subnetwork: "",
},
"failed wait global op": {
operationGlobalAPI: stubOperationGlobalAPI{waitErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
wantErr: true,
subnetwork: "subnetwork-id-1",
},
"failed delete networks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{deleteErr: someErr},
subnetworksAPI: stubSubnetworksAPI{},
wantErr: true,
subnetwork: "subnetwork-id-1",
},
"failed delete subnetworks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{deleteErr: someErr},
wantErr: true,
subnetwork: "subnetwork-id-1",
},
"must delete firewalls first": {
firewalls: []string{"firewall-1", "firewall-2"},
operationRegionAPI: stubOperationRegionAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
wantErr: true,
subnetwork: "subnetwork-id-1",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
operationGlobalAPI: tc.operationGlobalAPI,
operationRegionAPI: tc.operationRegionAPI,
networksAPI: tc.networksAPI,
subnetworksAPI: tc.subnetworksAPI,
firewalls: tc.firewalls,
network: "network-id-1",
subnetwork: tc.subnetwork,
}
if tc.wantErr {
assert.Error(client.TerminateVPCs(ctx))
} else {
assert.NoError(client.TerminateVPCs(ctx))
assert.Empty(client.network)
}
})
}
}
func TestCreateFirewall(t *testing.T) {
someErr := errors.New("failed")
testFirewallInput := FirewallInput{
Ingress: cloudtypes.Firewall{
cloudtypes.FirewallRule{
Name: "test-1",
Description: "test-1 description",
Protocol: "tcp",
IPRange: "192.0.2.0/24",
FromPort: 9000,
},
cloudtypes.FirewallRule{
Name: "test-2",
Description: "test-2 description",
Protocol: "udp",
IPRange: "192.0.2.0/24",
FromPort: 51820,
},
},
Egress: cloudtypes.Firewall{},
}
testCases := map[string]struct {
network string
operationGlobalAPI operationGlobalAPI
firewallsAPI firewallsAPI
firewallInput FirewallInput
wantErr bool
}{
"successful create": {
network: "network",
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{},
},
"failed wait global op": {
network: "network",
operationGlobalAPI: stubOperationGlobalAPI{waitErr: someErr},
firewallsAPI: stubFirewallsAPI{},
wantErr: true,
},
"failed insert networks": {
network: "network",
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{insertErr: someErr},
wantErr: true,
},
"no network set": {
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
network: tc.network,
operationGlobalAPI: tc.operationGlobalAPI,
firewallsAPI: tc.firewallsAPI,
}
if tc.wantErr {
assert.Error(client.CreateFirewall(ctx, testFirewallInput))
} else {
assert.NoError(client.CreateFirewall(ctx, testFirewallInput))
assert.ElementsMatch([]string{"test-1", "test-2"}, client.firewalls)
}
})
}
}
func TestTerminateFirewall(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
operationGlobalAPI operationGlobalAPI
firewallsAPI firewallsAPI
firewalls []string
wantErr bool
}{
"successful terminate": {
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{},
firewalls: []string{"firewall-1", "firewall-2"},
},
"successful terminate when no firewall exists": {
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{},
firewalls: []string{},
},
"failed to wait on global operation": {
operationGlobalAPI: stubOperationGlobalAPI{waitErr: someErr},
firewallsAPI: stubFirewallsAPI{},
firewalls: []string{"firewall-1", "firewall-2"},
wantErr: true,
},
"failed to delete firewalls": {
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{deleteErr: someErr},
firewalls: []string{"firewall-1", "firewall-2"},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
firewalls: tc.firewalls,
operationGlobalAPI: tc.operationGlobalAPI,
firewallsAPI: tc.firewallsAPI,
}
if tc.wantErr {
assert.Error(client.TerminateFirewall(ctx))
} else {
assert.NoError(client.TerminateFirewall(ctx))
assert.Empty(client.firewalls)
}
})
}
}

View file

@ -0,0 +1,101 @@
package client
import (
"context"
"errors"
"fmt"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
)
// waitForOperations waits until every operation in the opIDs slice is
// done or returns the first occurring error.
func (c *Client) waitForOperations(ctx context.Context, ops []Operation) error {
for _, op := range ops {
switch {
case op.Proto() == nil:
return errors.New("proto of operation is nil")
case op.Proto().Zone != nil:
if err := c.waitForZoneOperation(ctx, op); err != nil {
return err
}
case op.Proto().Region != nil:
if err := c.waitForRegionOperation(ctx, op); err != nil {
return err
}
default:
if err := c.waitForGlobalOperation(ctx, op); err != nil {
return err
}
}
}
return nil
}
func (c *Client) waitForGlobalOperation(ctx context.Context, op Operation) error {
for {
if err := ctx.Err(); err != nil {
return err
}
waitReq := &computepb.WaitGlobalOperationRequest{
Operation: *op.Proto().Name,
Project: c.project,
}
zoneOp, err := c.operationGlobalAPI.Wait(ctx, waitReq)
if err != nil {
return fmt.Errorf("unable to wait for the operation: %w", err)
}
if *zoneOp.Status.Enum() == computepb.Operation_DONE {
if opErr := zoneOp.Error; opErr != nil {
return fmt.Errorf("operation failed: %s", opErr.String())
}
return nil
}
}
}
func (c *Client) waitForZoneOperation(ctx context.Context, op Operation) error {
for {
if err := ctx.Err(); err != nil {
return err
}
waitReq := &computepb.WaitZoneOperationRequest{
Operation: *op.Proto().Name,
Project: c.project,
Zone: c.zone,
}
zoneOp, err := c.operationZoneAPI.Wait(ctx, waitReq)
if err != nil {
return fmt.Errorf("unable to wait for the operation: %w", err)
}
if *zoneOp.Status.Enum() == computepb.Operation_DONE {
if opErr := zoneOp.Error; opErr != nil {
return fmt.Errorf("operation failed: %s", opErr.String())
}
return nil
}
}
}
func (c *Client) waitForRegionOperation(ctx context.Context, op Operation) error {
for {
if err := ctx.Err(); err != nil {
return err
}
waitReq := &computepb.WaitRegionOperationRequest{
Operation: *op.Proto().Name,
Project: c.project,
Region: c.region,
}
regionOp, err := c.operationRegionAPI.Wait(ctx, waitReq)
if err != nil {
return fmt.Errorf("unable to wait for the operation: %w", err)
}
if *regionOp.Status.Enum() == computepb.Operation_DONE {
if opErr := regionOp.Error; opErr != nil {
return fmt.Errorf("operation failed: %s", opErr.String())
}
return nil
}
}
}

View file

@ -0,0 +1,71 @@
package client
import (
"context"
"fmt"
iampb "google.golang.org/genproto/googleapis/iam/v1"
)
// addIAMPolicyBindings adds a GCP service account to roles specified in the input.
func (c *Client) addIAMPolicyBindings(ctx context.Context, input AddIAMPolicyBindingInput) error {
getReq := &iampb.GetIamPolicyRequest{
Resource: "projects/" + c.project,
}
policy, err := c.projectsAPI.GetIamPolicy(ctx, getReq)
if err != nil {
return fmt.Errorf("retrieving current iam policy failed: %w", err)
}
for _, binding := range input.Bindings {
addIAMPolicy(policy, binding)
}
setReq := &iampb.SetIamPolicyRequest{
Resource: "projects/" + c.project,
Policy: policy,
}
if _, err := c.projectsAPI.SetIamPolicy(ctx, setReq); err != nil {
return fmt.Errorf("setting new iam policy failed: %w", err)
}
return nil
}
// PolicyBinding is a GCP IAM policy binding.
type PolicyBinding struct {
ServiceAccount string
Role string
}
// addIAMPolicy inserts policy binding for service account and role to an existing iam policy.
func addIAMPolicy(policy *iampb.Policy, policyBinding PolicyBinding) {
var binding *iampb.Binding
for _, existingBinding := range policy.Bindings {
if existingBinding.Role == policyBinding.Role && existingBinding.Condition == nil {
binding = existingBinding
break
}
}
if binding == nil {
binding = &iampb.Binding{
Role: policyBinding.Role,
}
policy.Bindings = append(policy.Bindings, binding)
}
// add service account to role, if not already a member
member := "serviceAccount:" + policyBinding.ServiceAccount
var alreadyMember bool
for _, existingMember := range binding.Members {
if member == existingMember {
alreadyMember = true
break
}
}
if !alreadyMember {
binding.Members = append(binding.Members, member)
}
}
// AddIAMPolicyBindingInput is the input for an AddIAMPolicyBinding operation.
type AddIAMPolicyBindingInput struct {
Bindings []PolicyBinding
}

View file

@ -0,0 +1,177 @@
package client
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
iampb "google.golang.org/genproto/googleapis/iam/v1"
"google.golang.org/protobuf/proto"
)
func TestAddIAMPolicyBindings(t *testing.T) {
someErr := errors.New("someErr")
testCases := map[string]struct {
projectsAPI stubProjectsAPI
input AddIAMPolicyBindingInput
wantErr bool
}{
"successful set without new bindings": {
input: AddIAMPolicyBindingInput{
Bindings: []PolicyBinding{},
},
},
"successful set with bindings": {
input: AddIAMPolicyBindingInput{
Bindings: []PolicyBinding{
{
ServiceAccount: "service-account",
Role: "role",
},
},
},
},
"retrieving iam policy fails": {
projectsAPI: stubProjectsAPI{
getPolicyErr: someErr,
},
wantErr: true,
},
"setting iam policy fails": {
projectsAPI: stubProjectsAPI{
setPolicyErr: someErr,
},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
projectsAPI: tc.projectsAPI,
}
err := client.addIAMPolicyBindings(ctx, tc.input)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestAddIAMPolicy(t *testing.T) {
testCases := map[string]struct {
binding PolicyBinding
policy *iampb.Policy
wantErr bool
wantPolicy *iampb.Policy
}{
"successful on empty policy": {
binding: PolicyBinding{
ServiceAccount: "service-account",
Role: "role",
},
policy: &iampb.Policy{
Bindings: []*iampb.Binding{},
},
wantPolicy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Role: "role",
Members: []string{"serviceAccount:service-account"},
},
},
},
},
"successful on existing policy with different role": {
binding: PolicyBinding{
ServiceAccount: "service-account",
Role: "role",
},
policy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Role: "other-role",
Members: []string{"other-member"},
},
},
},
wantPolicy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Role: "other-role",
Members: []string{"other-member"},
},
{
Role: "role",
Members: []string{"serviceAccount:service-account"},
},
},
},
},
"successful on existing policy with existing role": {
binding: PolicyBinding{
ServiceAccount: "service-account",
Role: "role",
},
policy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Role: "role",
Members: []string{"other-member"},
},
},
},
wantPolicy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Role: "role",
Members: []string{"other-member", "serviceAccount:service-account"},
},
},
},
},
"already a member": {
binding: PolicyBinding{
ServiceAccount: "service-account",
Role: "role",
},
policy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Role: "role",
Members: []string{"serviceAccount:service-account"},
},
},
},
wantPolicy: &iampb.Policy{
Bindings: []*iampb.Binding{
{
Role: "role",
Members: []string{"serviceAccount:service-account"},
},
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
addIAMPolicy(tc.policy, tc.binding)
assert.True(proto.Equal(tc.wantPolicy, tc.policy))
})
}
}

View file

@ -0,0 +1,117 @@
package client
import (
"context"
"encoding/json"
"fmt"
"github.com/edgelesssys/constellation/internal/gcpshared"
adminpb "google.golang.org/genproto/googleapis/iam/admin/v1"
)
// CreateServiceAccount creates a new GCP service account and returns an account key as service account URI.
func (c *Client) CreateServiceAccount(ctx context.Context, input ServiceAccountInput) (string, error) {
insertInput := insertServiceAccountInput{
Project: c.project,
AccountID: "constellation-app-" + c.uid,
DisplayName: "constellation-app-" + c.uid,
Description: "This service account belongs to a Constellation cluster.",
}
email, err := c.insertServiceAccount(ctx, insertInput)
if err != nil {
return "", err
}
c.serviceAccount = email
iamInput := input.addIAMPolicyBindingInput(c.serviceAccount)
if err := c.addIAMPolicyBindings(ctx, iamInput); err != nil {
return "", err
}
key, err := c.createServiceAccountKey(ctx, email)
if err != nil {
return "", err
}
return key.ToCloudServiceAccountURI(), nil
}
func (c *Client) TerminateServiceAccount(ctx context.Context) error {
if c.serviceAccount != "" {
req := &adminpb.DeleteServiceAccountRequest{
Name: "projects/-/serviceAccounts/" + c.serviceAccount,
}
if err := c.iamAPI.DeleteServiceAccount(ctx, req); err != nil {
return fmt.Errorf("deleting service account failed: %w", err)
}
c.serviceAccount = ""
}
return nil
}
type ServiceAccountInput struct {
Roles []string
}
func (i ServiceAccountInput) addIAMPolicyBindingInput(serviceAccount string) AddIAMPolicyBindingInput {
iamPolicyBindingInput := AddIAMPolicyBindingInput{
Bindings: make([]PolicyBinding, len(i.Roles)),
}
for i, role := range i.Roles {
iamPolicyBindingInput.Bindings[i] = PolicyBinding{
ServiceAccount: serviceAccount,
Role: role,
}
}
return iamPolicyBindingInput
}
func (c *Client) insertServiceAccount(ctx context.Context, input insertServiceAccountInput) (string, error) {
req := input.createServiceAccountRequest()
account, err := c.iamAPI.CreateServiceAccount(ctx, req)
if err != nil {
return "", err
}
return account.Email, nil
}
func (c *Client) createServiceAccountKey(ctx context.Context, email string) (gcpshared.ServiceAccountKey, error) {
req := createServiceAccountKeyRequest(email)
key, err := c.iamAPI.CreateServiceAccountKey(ctx, req)
if err != nil {
return gcpshared.ServiceAccountKey{}, fmt.Errorf("creating service account key failed: %w", err)
}
var serviceAccountKey gcpshared.ServiceAccountKey
if err := json.Unmarshal(key.PrivateKeyData, &serviceAccountKey); err != nil {
return gcpshared.ServiceAccountKey{}, fmt.Errorf("decoding service account key JSON failed: %w", err)
}
return serviceAccountKey, nil
}
// insertServiceAccountInput is the input for a createServiceAccount operation.
type insertServiceAccountInput struct {
Project string
AccountID string
DisplayName string
Description string
}
func (c insertServiceAccountInput) createServiceAccountRequest() *adminpb.CreateServiceAccountRequest {
return &adminpb.CreateServiceAccountRequest{
Name: "projects/" + c.Project,
AccountId: c.AccountID,
ServiceAccount: &adminpb.ServiceAccount{
DisplayName: c.DisplayName,
Description: c.Description,
},
}
}
func createServiceAccountKeyRequest(email string) *adminpb.CreateServiceAccountKeyRequest {
return &adminpb.CreateServiceAccountKeyRequest{
Name: "projects/-/serviceAccounts/" + email,
}
}

View file

@ -0,0 +1,139 @@
package client
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/edgelesssys/constellation/internal/gcpshared"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateServiceAccount(t *testing.T) {
require := require.New(t)
someErr := errors.New("someErr")
key := gcpshared.ServiceAccountKey{
Type: "type",
ProjectID: "project-id",
PrivateKeyID: "private-key-id",
PrivateKey: "private-key",
ClientEmail: "client-email",
ClientID: "client-id",
AuthURI: "auth-uri",
TokenURI: "token-uri",
AuthProviderX509CertURL: "auth-provider-x509-cert-url",
ClientX509CertURL: "client-x509-cert-url",
}
keyData, err := json.Marshal(key)
require.NoError(err)
testCases := map[string]struct {
iamAPI iamAPI
projectsAPI stubProjectsAPI
input ServiceAccountInput
wantErr bool
}{
"successful create": {
iamAPI: stubIAMAPI{serviceAccountKeyData: keyData},
input: ServiceAccountInput{
Roles: []string{"someRole"},
},
},
"successful create with roles": {
iamAPI: stubIAMAPI{serviceAccountKeyData: keyData},
},
"creating account fails": {
iamAPI: stubIAMAPI{createErr: someErr},
wantErr: true,
},
"creating account key fails": {
iamAPI: stubIAMAPI{createKeyErr: someErr},
wantErr: true,
},
"key data missing": {
iamAPI: stubIAMAPI{},
wantErr: true,
},
"key data corrupt": {
iamAPI: stubIAMAPI{serviceAccountKeyData: []byte("invalid key data")},
wantErr: true,
},
"retrieving iam policy bindings fails": {
iamAPI: stubIAMAPI{},
projectsAPI: stubProjectsAPI{getPolicyErr: someErr},
wantErr: true,
},
"setting iam policy bindings fails": {
iamAPI: stubIAMAPI{},
projectsAPI: stubProjectsAPI{setPolicyErr: someErr},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
iamAPI: tc.iamAPI,
projectsAPI: tc.projectsAPI,
}
serviceAccountKey, err := client.CreateServiceAccount(ctx, tc.input)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(key.ToCloudServiceAccountURI(), serviceAccountKey)
assert.Equal("email", client.serviceAccount)
}
})
}
}
func TestTerminateServiceAccount(t *testing.T) {
testCases := map[string]struct {
iamAPI iamAPI
wantErr bool
}{
"delete works": {
iamAPI: stubIAMAPI{},
},
"delete fails": {
iamAPI: stubIAMAPI{
deleteServiceAccountErr: errors.New("someErr"),
},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
serviceAccount: "service-account",
iamAPI: tc.iamAPI,
}
err := client.TerminateServiceAccount(ctx)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}

View file

@ -0,0 +1,14 @@
package gcp
// InstanceTypes are valid GCP instance types.
var InstanceTypes = []string{
"n2d-standard-2",
"n2d-standard-4",
"n2d-standard-8",
"n2d-standard-16",
"n2d-standard-32",
"n2d-standard-48",
"n2d-standard-64",
"n2d-standard-80",
"n2d-standard-96",
}

View file

@ -0,0 +1,4 @@
package gcp
// KubeEnv contains placeholder values required by cluster-autoscaler.
var KubeEnv = `AUTOSCALER_ENV_VARS: kube_reserved=cpu=1060m,memory=1019Mi,ephemeral-storage=41Gi;node_labels=;os=linux;os_distribution=cos;evictionHard=`