Co-authored-by: Malte Poll <mp@edgeless.systems>
Co-authored-by: katexochen <katexochen@users.noreply.github.com>
Co-authored-by: Daniel Weiße <dw@edgeless.systems>
Co-authored-by: Thomas Tendyck <tt@edgeless.systems>
Co-authored-by: Benedict Schlueter <bs@edgeless.systems>
Co-authored-by: leongross <leon.gross@rub.de>
Co-authored-by: Moritz Eckert <m1gh7ym0@gmail.com>
This commit is contained in:
Leonard Cohnen 2022-03-22 16:03:15 +01:00
commit 2d8fcd9bf4
362 changed files with 50980 additions and 0 deletions

101
cli/gcp/client/api.go Normal file
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)
}

413
cli/gcp/client/api_test.go Normal file
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
}

384
cli/gcp/client/client.go Normal file
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/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"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 gcp.Instances
coordinators gcp.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(gcp.Instances),
coordinators: make(gcp.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/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"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
errExpected bool
}{
"valid state": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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: gcp.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",
},
errExpected: true,
},
"missing coordinator": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.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",
},
errExpected: true,
},
"missing node group": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing coordinator group": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing project id": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing zone": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing region": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing name": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing uid": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing firewalls": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing network": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing external network": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing subnetwork": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing external subnetwork": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing node template": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: true,
},
"missing coordinator template": {
state: state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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",
},
errExpected: 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.errExpected {
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.errExpected {
_, 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: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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: gcp.Instances{
"id-1": {
PublicIP: "ip1",
PrivateIP: "ip2",
},
},
GCPCoordinators: gcp.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)
}

393
cli/gcp/client/instances.go Normal file
View file

@ -0,0 +1,393 @@
package client
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/edgelesssys/constellation/cli/gcp"
"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,
KubeEnv: input.KubeEnv,
Project: c.project,
Zone: c.zone,
Region: c.region,
UID: c.uid,
DisableCVM: input.DisableCVM,
}
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,
ImageId: input.ImageId,
InstanceType: input.InstanceType,
KubeEnv: input.KubeEnv,
Project: c.project,
Zone: c.zone,
Region: c.region,
UID: c.uid,
DisableCVM: input.DisableCVM,
}
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{}
nodeGroupInput := instanceGroupManagerInput{
Count: input.Count - 1,
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
coordinatorGroupInput := instanceGroupManagerInput{
Count: 1,
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
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(gcp.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(gcp.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 gcp.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 := gcp.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 {
Count int
ImageId string
InstanceType string
KubeEnv string
DisableCVM bool
}
type insertInstanceTemplateInput struct {
Name string
Network string
Subnetwork string
SecondarySubnetworkRangeName string
ImageId string
InstanceType string
KubeEnv string
Project string
Zone string
Region string
UID string
DisableCVM bool
}
func (i insertInstanceTemplateInput) insertInstanceTemplateRequest() *computepb.InsertInstanceTemplateRequest {
req := computepb.InsertInstanceTemplateRequest{
InstanceTemplateResource: &computepb.InstanceTemplate{
Description: proto.String("This instance belongs to a Constellation."),
Name: proto.String(i.Name),
Properties: &computepb.InstanceProperties{
ConfidentialInstanceConfig: &computepb.ConfidentialInstanceConfig{
EnableConfidentialCompute: proto.Bool(!i.DisableCVM),
},
Description: proto.String("This instance belongs to a Constellation."),
Disks: []*computepb.AttachedDisk{
{
InitializeParams: &computepb.AttachedDiskInitializeParams{
DiskSizeGb: proto.Int64(10),
SourceImage: proto.String("projects/" + i.Project + "/global/images/" + i.ImageId),
},
AutoDelete: proto.Bool(true),
Boot: proto.Bool(true),
Mode: proto.String(computepb.AttachedDisk_READ_WRITE.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),
},
},
},
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"},
},
},
},
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,262 @@
package client
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/gcp"
"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{
Count: 3,
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
errExpected 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,
errExpected: 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,
errExpected: 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,
errExpected: 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",
errExpected: true,
},
"failed insert instanceGroupManager": {
instanceAPI: stubInstanceAPI{listIterator: &stubInstanceIterator{instances: testInstances}},
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{insertErr: someErr},
network: "network",
input: testInput,
errExpected: 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,
errExpected: 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,
errExpected: 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(gcp.Instances),
coordinators: make(gcp.Instances),
}
if tc.errExpected {
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
errExpected 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},
errExpected: true,
},
"fail delete instanceTemplate": {
operationZoneAPI: stubOperationZoneAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
instanceTemplateAPI: stubInstanceTemplateAPI{deleteErr: someErr},
instanceGroupManagersAPI: stubInstanceGroupManagersAPI{},
errExpected: 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: gcp.Instances{"node-id-1": gcp.Instance{}, "node-id-2": gcp.Instance{}},
coordinators: gcp.Instances{"coordinator-id-1": gcp.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 = gcp.Instances{}
}
if tc.errExpected {
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)
}
})
}
}

202
cli/gcp/client/network.go Normal file
View file

@ -0,0 +1,202 @@
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"
)
// 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 := input.Ingress.GCP()
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
}
// VPCsInput defines the VPC configuration.
type VPCsInput struct {
SubnetCIDR string
SubnetExtCIDR string
}
// CreateVPCs creates all necessary VPC networks.
func (c *Client) CreateVPCs(ctx context.Context, input VPCsInput) 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, input.SubnetCIDR); 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.terminateSubnets(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, subnetCIDR string) error {
c.subnetwork = "node-net-" + c.uid
c.secondarySubnetworkRange = "net-ext" + c.uid
op, err := c.createSubnet(ctx, c.subnetwork, subnetCIDR, c.network, c.secondarySubnetworkRange)
if err != nil {
return err
}
return c.waitForOperations(ctx, []Operation{op})
}
func (c *Client) createSubnet(ctx context.Context, name, cidr, network, secondaryRangeName string) (Operation, error) {
req := &computepb.InsertSubnetworkRequest{
Project: c.project,
Region: c.region,
SubnetworkResource: &computepb.Subnetwork{
IpCidrRange: proto.String(cidr),
Name: proto.String(name),
Network: proto.String("projects/" + c.project + "/global/networks/" + network),
SecondaryIpRanges: []*computepb.SubnetworkSecondaryRange{
{
RangeName: proto.String(secondaryRangeName),
IpCidrRange: proto.String("10.10.0.0/16"),
},
},
},
}
return c.subnetworksAPI.Insert(ctx, req)
}
func (c *Client) terminateSubnets(ctx context.Context) error {
var op Operation
var err error
if c.subnetwork != "" {
op, err = c.terminateSubnet(ctx, c.subnetwork)
if err != nil {
return err
}
}
return c.waitForOperations(ctx, []Operation{op})
}
func (c *Client) terminateSubnet(ctx context.Context, name string) (Operation, error) {
req := &computepb.DeleteSubnetworkRequest{
Project: c.project,
Region: c.region,
Subnetwork: name,
}
return c.subnetworksAPI.Delete(ctx, req)
}

View file

@ -0,0 +1,302 @@
package client
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloud/cloudtypes"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/stretchr/testify/assert"
)
func TestCreateVPCs(t *testing.T) {
someErr := errors.New("failed")
testInput := VPCsInput{
SubnetCIDR: "192.0.2.0/24",
SubnetExtCIDR: "198.51.100.0/24",
}
testCases := map[string]struct {
operationGlobalAPI operationGlobalAPI
operationRegionAPI operationRegionAPI
networksAPI networksAPI
subnetworksAPI subnetworksAPI
errExpected 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{},
errExpected: true,
},
"failed wait region op": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{waitErr: someErr},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
errExpected: true,
},
"failed insert networks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{insertErr: someErr},
subnetworksAPI: stubSubnetworksAPI{},
errExpected: true,
},
"failed insert subnetworks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{insertErr: someErr},
errExpected: 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(gcp.Instances),
coordinators: make(gcp.Instances),
}
if tc.errExpected {
assert.Error(client.CreateVPCs(ctx, testInput))
} else {
assert.NoError(client.CreateVPCs(ctx, testInput))
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
errExpected bool
}{
"successful terminate": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
},
"failed wait global op": {
operationGlobalAPI: stubOperationGlobalAPI{waitErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
errExpected: true,
},
"failed delete networks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{deleteErr: someErr},
subnetworksAPI: stubSubnetworksAPI{},
errExpected: true,
},
"failed delete subnetworks": {
operationGlobalAPI: stubOperationGlobalAPI{},
operationRegionAPI: stubOperationRegionAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{deleteErr: someErr},
errExpected: true,
},
"must delete firewalls first": {
firewalls: []string{"firewall-1", "firewall-2"},
operationRegionAPI: stubOperationRegionAPI{},
operationGlobalAPI: stubOperationGlobalAPI{},
networksAPI: stubNetworksAPI{},
subnetworksAPI: stubSubnetworksAPI{},
errExpected: 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,
firewalls: tc.firewalls,
network: "network-id-1",
subnetwork: "subnetwork-id-1",
}
if tc.errExpected {
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",
Port: 9000,
},
cloudtypes.FirewallRule{
Name: "test-2",
Description: "test-2 description",
Protocol: "udp",
IPRange: "192.0.2.0/24",
Port: 51820,
},
},
Egress: cloudtypes.Firewall{},
}
testCases := map[string]struct {
network string
operationGlobalAPI operationGlobalAPI
firewallsAPI firewallsAPI
firewallInput FirewallInput
errExpected bool
}{
"successful create": {
network: "network",
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{},
},
"failed wait global op": {
network: "network",
operationGlobalAPI: stubOperationGlobalAPI{waitErr: someErr},
firewallsAPI: stubFirewallsAPI{},
errExpected: true,
},
"failed insert networks": {
network: "network",
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{insertErr: someErr},
errExpected: true,
},
"no network set": {
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{},
errExpected: 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.errExpected {
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
errExpected 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"},
errExpected: true,
},
"failed to delete firewalls": {
operationGlobalAPI: stubOperationGlobalAPI{},
firewallsAPI: stubFirewallsAPI{deleteErr: someErr},
firewalls: []string{"firewall-1", "firewall-2"},
errExpected: 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.errExpected {
assert.Error(client.TerminateFirewall(ctx))
} else {
assert.NoError(client.TerminateFirewall(ctx))
assert.Empty(client.firewalls)
}
})
}
}

View file

@ -0,0 +1,98 @@
package client
import (
"context"
"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().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
}
}
}

71
cli/gcp/client/project.go Normal file
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
errExpected 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,
},
errExpected: true,
},
"setting iam policy fails": {
projectsAPI: stubProjectsAPI{
setPolicyErr: someErr,
},
errExpected: 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.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestAddIAMPolicy(t *testing.T) {
testCases := map[string]struct {
binding PolicyBinding
policy *iampb.Policy
errExpected bool
policyExpected *iampb.Policy
}{
"successful on empty policy": {
binding: PolicyBinding{
ServiceAccount: "service-account",
Role: "role",
},
policy: &iampb.Policy{
Bindings: []*iampb.Binding{},
},
policyExpected: &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"},
},
},
},
policyExpected: &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"},
},
},
},
policyExpected: &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"},
},
},
},
policyExpected: &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.policyExpected, tc.policy))
})
}
}

View file

@ -0,0 +1,152 @@
package client
import (
"context"
"encoding/json"
"fmt"
"net/url"
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.",
}
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.ConvertToCloudServiceAccountURI(), 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
}
// ServiceAccountKey is a GCP service account key.
type ServiceAccountKey struct {
Type string `json:"type"`
ProjectID string `json:"project_id"`
PrivateKeyID string `json:"private_key_id"`
PrivateKey string `json:"private_key"`
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
AuthURI string `json:"auth_uri"`
TokenURI string `json:"token_uri"`
AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
ClientX509CertURL string `json:"client_x509_cert_url"`
}
// ConvertToCloudServiceAccountURI converts the ServiceAccountKey into a cloud service account URI.
func (k ServiceAccountKey) ConvertToCloudServiceAccountURI() string {
query := url.Values{}
query.Add("type", k.Type)
query.Add("project_id", k.ProjectID)
query.Add("private_key_id", k.PrivateKeyID)
query.Add("private_key", k.PrivateKey)
query.Add("client_email", k.ClientEmail)
query.Add("client_id", k.ClientID)
query.Add("auth_uri", k.AuthURI)
query.Add("token_uri", k.TokenURI)
query.Add("auth_provider_x509_cert_url", k.AuthProviderX509CertURL)
query.Add("client_x509_cert_url", k.ClientX509CertURL)
uri := url.URL{
Scheme: "serviceaccount",
Host: "gcp",
RawQuery: query.Encode(),
}
return uri.String()
}
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) (ServiceAccountKey, error) {
req := createServiceAccountKeyRequest(email)
key, err := c.iamAPI.CreateServiceAccountKey(ctx, req)
if err != nil {
return ServiceAccountKey{}, fmt.Errorf("creating service account key failed: %w", err)
}
var serviceAccountKey ServiceAccountKey
if err := json.Unmarshal(key.PrivateKeyData, &serviceAccountKey); err != nil {
return 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,174 @@
package client
import (
"context"
"encoding/json"
"errors"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateServiceAccount(t *testing.T) {
require := require.New(t)
someErr := errors.New("someErr")
key := 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
errExpected 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},
errExpected: true,
},
"creating account key fails": {
iamAPI: stubIAMAPI{createKeyErr: someErr},
errExpected: true,
},
"key data missing": {
iamAPI: stubIAMAPI{},
errExpected: true,
},
"key data corrupt": {
iamAPI: stubIAMAPI{serviceAccountKeyData: []byte("invalid key data")},
errExpected: true,
},
"retrieving iam policy bindings fails": {
iamAPI: stubIAMAPI{},
projectsAPI: stubProjectsAPI{getPolicyErr: someErr},
errExpected: true,
},
"setting iam policy bindings fails": {
iamAPI: stubIAMAPI{},
projectsAPI: stubProjectsAPI{setPolicyErr: someErr},
errExpected: 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.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(key.ConvertToCloudServiceAccountURI(), serviceAccountKey)
assert.Equal("email", client.serviceAccount)
}
})
}
}
func TestTerminateServiceAccount(t *testing.T) {
testCases := map[string]struct {
iamAPI iamAPI
errExpected bool
}{
"delete works": {
iamAPI: stubIAMAPI{},
},
"delete fails": {
iamAPI: stubIAMAPI{
deleteServiceAccountErr: errors.New("someErr"),
},
errExpected: 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.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestConvertToCloudServiceAccountURI(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
key := 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",
}
cloudServiceAccountURI := key.ConvertToCloudServiceAccountURI()
uri, err := url.Parse(cloudServiceAccountURI)
require.NoError(err)
query := uri.Query()
assert.Equal("serviceaccount", uri.Scheme)
assert.Equal("gcp", uri.Host)
assert.Equal(url.Values{
"type": []string{"type"},
"project_id": []string{"project-id"},
"private_key_id": []string{"private-key-id"},
"private_key": []string{"private-key"},
"client_email": []string{"client-email"},
"client_id": []string{"client-id"},
"auth_uri": []string{"auth-uri"},
"token_uri": []string{"token-uri"},
"auth_provider_x509_cert_url": []string{"auth-provider-x509-cert-url"},
"client_x509_cert_url": []string{"client-x509-cert-url"},
}, query)
}