Use multiple loadbalancers on GCP

This commit is contained in:
katexochen 2022-08-01 16:51:34 +02:00 committed by Paul Meyer
parent c954ec089f
commit a02a46e454
59 changed files with 1629 additions and 557 deletions

View file

@ -172,7 +172,7 @@ func (c *Client) GetState() state.ConstellationState {
Name: c.name,
UID: c.uid,
CloudProvider: cloudprovider.Azure.String(),
BootstrapperHost: c.loadBalancerPubIP,
LoadBalancerIP: c.loadBalancerPubIP,
AzureLocation: c.location,
AzureSubscription: c.subscriptionID,
AzureTenant: c.tenantID,
@ -192,7 +192,7 @@ func (c *Client) SetState(stat state.ConstellationState) {
c.resourceGroup = stat.AzureResourceGroup
c.name = stat.Name
c.uid = stat.UID
c.loadBalancerPubIP = stat.BootstrapperHost
c.loadBalancerPubIP = stat.LoadBalancerIP
c.location = stat.AzureLocation
c.subscriptionID = stat.AzureSubscription
c.tenantID = stat.AzureTenant

View file

@ -26,7 +26,7 @@ func TestSetGetState(t *testing.T) {
},
Name: "name",
UID: "uid",
BootstrapperHost: "bootstrapper-host",
LoadBalancerIP: "bootstrapper-host",
AzureResourceGroup: "resource-group",
AzureLocation: "location",
AzureSubscription: "subscription",
@ -64,7 +64,7 @@ func TestSetGetState(t *testing.T) {
controlPlanes: state.AzureControlPlaneInstances,
name: state.Name,
uid: state.UID,
loadBalancerPubIP: state.BootstrapperHost,
loadBalancerPubIP: state.LoadBalancerIP,
resourceGroup: state.AzureResourceGroup,
location: state.AzureLocation,
subscriptionID: state.AzureSubscription,

View file

@ -62,7 +62,7 @@ func (l LoadBalancer) Azure() armnetwork.LoadBalancer {
Name: to.Ptr(kubeHealthProbeName),
Properties: &armnetwork.ProbePropertiesFormat{
Protocol: to.Ptr(armnetwork.ProbeProtocolTCP),
Port: to.Ptr(int32(6443)),
Port: to.Ptr(int32(constants.KubernetesPort)),
},
},
{
@ -83,7 +83,7 @@ func (l LoadBalancer) Azure() armnetwork.LoadBalancer {
Name: to.Ptr(debugdHealthProbeName),
Properties: &armnetwork.ProbePropertiesFormat{
Protocol: to.Ptr(armnetwork.ProbeProtocolTCP),
Port: to.Ptr[int32](4000),
Port: to.Ptr[int32](constants.DebugdPort),
},
},
},
@ -94,8 +94,8 @@ func (l LoadBalancer) Azure() armnetwork.LoadBalancer {
FrontendIPConfiguration: &armnetwork.SubResource{
ID: to.Ptr("/subscriptions/" + l.Subscription + "/resourceGroups/" + l.ResourceGroup + "/providers/Microsoft.Network/loadBalancers/" + l.Name + "/frontendIPConfigurations/" + frontEndIPConfigName),
},
FrontendPort: to.Ptr[int32](6443),
BackendPort: to.Ptr[int32](6443),
FrontendPort: to.Ptr[int32](constants.KubernetesPort),
BackendPort: to.Ptr[int32](constants.KubernetesPort),
Protocol: to.Ptr(armnetwork.TransportProtocolTCP),
Probe: &armnetwork.SubResource{
ID: to.Ptr("/subscriptions/" + l.Subscription + "/resourceGroups/" + l.ResourceGroup + "/providers/Microsoft.Network/loadBalancers/" + l.Name + "/probes/" + kubeHealthProbeName),
@ -154,8 +154,8 @@ func (l LoadBalancer) Azure() armnetwork.LoadBalancer {
FrontendIPConfiguration: &armnetwork.SubResource{
ID: to.Ptr("/subscriptions/" + l.Subscription + "/resourceGroups/" + l.ResourceGroup + "/providers/Microsoft.Network/loadBalancers/" + l.Name + "/frontendIPConfigurations/" + frontEndIPConfigName),
},
FrontendPort: to.Ptr[int32](4000),
BackendPort: to.Ptr[int32](4000),
FrontendPort: to.Ptr[int32](constants.DebugdPort),
BackendPort: to.Ptr[int32](constants.DebugdPort),
Protocol: to.Ptr(armnetwork.TransportProtocolTCP),
Probe: &armnetwork.SubResource{
ID: to.Ptr("/subscriptions/" + l.Subscription + "/resourceGroups/" + l.ResourceGroup + "/providers/Microsoft.Network/loadBalancers/" + l.Name + "/probes/" + debugdHealthProbeName),

View file

@ -14,11 +14,11 @@ type gcpclient interface {
CreateVPCs(ctx context.Context) error
CreateFirewall(ctx context.Context, input gcpcl.FirewallInput) error
CreateInstances(ctx context.Context, input gcpcl.CreateInstancesInput) error
CreateLoadBalancer(ctx context.Context) error
CreateLoadBalancers(ctx context.Context) error
CreateServiceAccount(ctx context.Context, input gcpcl.ServiceAccountInput) (string, error)
TerminateFirewall(ctx context.Context) error
TerminateVPCs(context.Context) error
TerminateLoadBalancer(context.Context) error
TerminateLoadBalancers(context.Context) error
TerminateInstances(context.Context) error
TerminateServiceAccount(ctx context.Context) error
Close() error

View file

@ -245,11 +245,7 @@ type fakeGcpClient struct {
name string
zone string
serviceAccount string
// loadbalancer
healthCheck string
backendService string
forwardingRule string
loadbalancers []string
}
func (c *fakeGcpClient) GetState() state.ConstellationState {
@ -264,14 +260,12 @@ func (c *fakeGcpClient) GetState() state.ConstellationState {
GCPNetwork: c.network,
GCPSubnetwork: c.subnetwork,
GCPFirewalls: c.firewalls,
GCPBackendService: c.backendService,
GCPHealthCheck: c.healthCheck,
GCPForwardingRule: c.forwardingRule,
GCPProject: c.project,
Name: c.name,
UID: c.uid,
GCPZone: c.zone,
GCPServiceAccount: c.serviceAccount,
GCPLoadbalancers: c.loadbalancers,
}
}
@ -290,9 +284,7 @@ func (c *fakeGcpClient) SetState(stat state.ConstellationState) {
c.uid = stat.UID
c.zone = stat.GCPZone
c.serviceAccount = stat.GCPServiceAccount
c.healthCheck = stat.GCPHealthCheck
c.backendService = stat.GCPBackendService
c.forwardingRule = stat.GCPForwardingRule
c.loadbalancers = stat.GCPLoadbalancers
}
func (c *fakeGcpClient) CreateVPCs(ctx context.Context) error {
@ -345,10 +337,8 @@ func (c *fakeGcpClient) CreateServiceAccount(ctx context.Context, input gcpcl.Se
}.ToCloudServiceAccountURI(), nil
}
func (c *fakeGcpClient) CreateLoadBalancer(ctx context.Context) error {
c.healthCheck = "health-check"
c.backendService = "backend-service"
c.forwardingRule = "forwarding-rule"
func (c *fakeGcpClient) CreateLoadBalancers(ctx context.Context) error {
c.loadbalancers = []string{"kube-lb", "boot-lb", "verify-lb"}
return nil
}
@ -384,10 +374,8 @@ func (c *fakeGcpClient) TerminateServiceAccount(context.Context) error {
return nil
}
func (c *fakeGcpClient) TerminateLoadBalancer(context.Context) error {
c.healthCheck = ""
c.backendService = ""
c.forwardingRule = ""
func (c *fakeGcpClient) TerminateLoadBalancers(context.Context) error {
c.loadbalancers = nil
return nil
}
@ -438,7 +426,7 @@ func (c *stubGcpClient) CreateServiceAccount(ctx context.Context, input gcpcl.Se
return gcpshared.ServiceAccountKey{}.ToCloudServiceAccountURI(), c.createServiceAccountErr
}
func (c *stubGcpClient) CreateLoadBalancer(ctx context.Context) error {
func (c *stubGcpClient) CreateLoadBalancers(ctx context.Context) error {
return c.createLoadBalancerErr
}
@ -462,7 +450,7 @@ func (c *stubGcpClient) TerminateServiceAccount(context.Context) error {
return c.terminateServiceAccountErr
}
func (c *stubGcpClient) TerminateLoadBalancer(context.Context) error {
func (c *stubGcpClient) TerminateLoadBalancers(context.Context) error {
return c.terminateLoadBalancerErr
}

View file

@ -133,7 +133,7 @@ func (c *Creator) createGCP(ctx context.Context, cl gcpclient, config *config.Co
return state.ConstellationState{}, err
}
if err := cl.CreateLoadBalancer(ctx); err != nil {
if err := cl.CreateLoadBalancers(ctx); err != nil {
return state.ConstellationState{}, err
}

View file

@ -32,9 +32,7 @@ func TestCreator(t *testing.T) {
GCPControlPlaneInstanceTemplate: "controlplane-template",
GCPNetwork: "network",
GCPSubnetwork: "subnetwork",
GCPBackendService: "backend-service",
GCPHealthCheck: "health-check",
GCPForwardingRule: "forwarding-rule",
GCPLoadbalancers: []string{"kube-lb", "boot-lb", "verify-lb"},
GCPFirewalls: []string{
"bootstrapper", "ssh", "nodeport", "kubernetes",
"allow-cluster-internal-tcp", "allow-cluster-internal-udp", "allow-cluster-internal-icmp",

View file

@ -34,7 +34,7 @@ type rollbackerGCP struct {
func (r *rollbackerGCP) rollback(ctx context.Context) error {
var err error
err = multierr.Append(err, r.client.TerminateLoadBalancer(ctx))
err = multierr.Append(err, r.client.TerminateLoadBalancers(ctx))
err = multierr.Append(err, r.client.TerminateInstances(ctx))
err = multierr.Append(err, r.client.TerminateFirewall(ctx))
err = multierr.Append(err, r.client.TerminateVPCs(ctx))

View file

@ -53,7 +53,7 @@ func (t *Terminator) Terminate(ctx context.Context, state state.ConstellationSta
func (t *Terminator) terminateGCP(ctx context.Context, cl gcpclient, state state.ConstellationState) error {
cl.SetState(state)
if err := cl.TerminateLoadBalancer(ctx); err != nil {
if err := cl.TerminateLoadBalancers(ctx); err != nil {
return err
}
if err := cl.TerminateInstances(ctx); err != nil {

View file

@ -211,7 +211,7 @@ func checkDirClean(fileHandler file.Handler) error {
}
func writeIPtoIDFile(fileHandler file.Handler, state state.ConstellationState) error {
ip := state.BootstrapperHost
ip := state.LoadBalancerIP
if ip == "" {
return fmt.Errorf("bootstrapper ip not found")
}

View file

@ -46,7 +46,7 @@ func TestCreateArgumentValidation(t *testing.T) {
}
func TestCreate(t *testing.T) {
testState := state.ConstellationState{Name: "test", BootstrapperHost: "192.0.2.1"}
testState := state.ConstellationState{Name: "test", LoadBalancerIP: "192.0.2.1"}
someErr := errors.New("failed")
testCases := map[string]struct {

View file

@ -141,6 +141,7 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
return fmt.Errorf("loading Helm charts: %w", err)
}
cmd.Println("Initializing cluster ...")
req := &initproto.InitRequest{
AutoscalingNodeGroups: autoscalingNodeGroups,
MasterSecret: flags.masterSecret.Key,

View file

@ -92,7 +92,7 @@ func TestVerify(t *testing.T) {
nodeEndpointFlag: "192.0.2.1",
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
wantEndpoint: "192.0.2.1:30081",
wantEndpoint: "192.0.2.1:" + strconv.Itoa(constants.VerifyServiceNodePortGRPC),
},
"endpoint not set": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
@ -107,7 +107,7 @@ func TestVerify(t *testing.T) {
ownerIDFlag: zeroBase64,
protoClient: &stubVerifyClient{},
idFile: &clusterIDsFile{IP: "192.0.2.1"},
wantEndpoint: "192.0.2.1:30081",
wantEndpoint: "192.0.2.1:" + strconv.Itoa(constants.VerifyServiceNodePortGRPC),
},
"override endpoint from details file": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },

View file

@ -105,15 +105,30 @@ type instanceGroupManagersAPI interface {
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
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)
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 addressesAPI interface {
Close() error
Insert(ctx context.Context, req *computepb.InsertAddressRequest,
opts ...gax.CallOption) (Operation, error)
Get(ctx context.Context, req *computepb.GetAddressRequest,
opts ...gax.CallOption) (*computepb.Address, error)
Delete(ctx context.Context, req *computepb.DeleteAddressRequest,
opts ...gax.CallOption) (Operation, error)
}
type Operation interface {

View file

@ -548,3 +548,48 @@ func (i *stubManagedInstanceIterator) Next() (*computepb.ManagedInstance, error)
i.internalCounter++
return resp, nil
}
type stubAddressesAPI struct {
insertErr error
getAddr *string
getErr error
deleteErr error
}
func (a stubAddressesAPI) Insert(context.Context, *computepb.InsertAddressRequest,
...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 stubAddressesAPI) Get(ctx context.Context, req *computepb.GetAddressRequest,
opts ...gax.CallOption,
) (*computepb.Address, error) {
return &computepb.Address{Address: a.getAddr}, a.getErr
}
func (a stubAddressesAPI) Delete(context.Context, *computepb.DeleteAddressRequest,
...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
}
func (a stubAddressesAPI) Close() error {
return nil
}

View file

@ -36,6 +36,7 @@ type Client struct {
instanceGroupManagersAPI
iamAPI
projectsAPI
addressesAPI
workers cloudtypes.Instances
controlPlanes cloudtypes.Instances
@ -56,9 +57,9 @@ type Client struct {
serviceAccount string
// loadbalancer
healthCheck string
backendService string
forwardingRule string
loadbalancerIP string
loadbalancerIPname string
loadbalancers []*loadBalancer
}
// NewFromDefault creates an uninitialized client.
@ -152,6 +153,13 @@ func NewFromDefault(ctx context.Context) (*Client, error) {
_ = closeAll(closers)
return nil, err
}
closers = append(closers, projectsAPI)
addressesAPI, err := compute.NewAddressesRESTClient(ctx)
if err != nil {
_ = closeAll(closers)
return nil, err
}
return &Client{
instanceAPI: &instanceClient{insAPI},
operationRegionAPI: opRegionAPI,
@ -167,6 +175,7 @@ func NewFromDefault(ctx context.Context) (*Client, error) {
instanceGroupManagersAPI: &instanceGroupManagersClient{groupAPI},
iamAPI: &iamClient{iamAPI},
projectsAPI: &projectsClient{projectsAPI},
addressesAPI: &addressesClient{addressesAPI},
workers: make(cloudtypes.Instances),
controlPlanes: make(cloudtypes.Instances),
}, nil
@ -221,6 +230,7 @@ func (c *Client) Close() error {
c.instanceGroupManagersAPI,
c.iamAPI,
c.projectsAPI,
c.addressesAPI,
}
return closeAll(closers)
}
@ -242,11 +252,11 @@ func (c *Client) init(project, zone, region, name string) error {
// GetState returns the state of the client as ConstellationState.
func (c *Client) GetState() state.ConstellationState {
return state.ConstellationState{
stat := state.ConstellationState{
Name: c.name,
UID: c.uid,
CloudProvider: cloudprovider.GCP.String(),
BootstrapperHost: c.controlPlanes.PublicIPs()[0],
LoadBalancerIP: c.loadbalancerIP,
GCPProject: c.project,
GCPZone: c.zone,
GCPRegion: c.region,
@ -259,11 +269,13 @@ func (c *Client) GetState() state.ConstellationState {
GCPFirewalls: c.firewalls,
GCPNetwork: c.network,
GCPSubnetwork: c.subnetwork,
GCPHealthCheck: c.healthCheck,
GCPBackendService: c.backendService,
GCPForwardingRule: c.forwardingRule,
GCPServiceAccount: c.serviceAccount,
GCPLoadbalancerIPname: c.loadbalancerIPname,
}
for _, lb := range c.loadbalancers {
stat.GCPLoadbalancers = append(stat.GCPLoadbalancers, lb.name)
}
return stat
}
// SetState sets the state of the client to the handed ConstellationState.
@ -282,10 +294,18 @@ func (c *Client) SetState(stat state.ConstellationState) {
c.subnetwork = stat.GCPSubnetwork
c.workerTemplate = stat.GCPWorkerInstanceTemplate
c.controlPlaneTemplate = stat.GCPControlPlaneInstanceTemplate
c.healthCheck = stat.GCPHealthCheck
c.backendService = stat.GCPBackendService
c.forwardingRule = stat.GCPForwardingRule
c.loadbalancerIPname = stat.GCPLoadbalancerIPname
c.loadbalancerIP = stat.LoadBalancerIP
c.serviceAccount = stat.GCPServiceAccount
for _, lbName := range stat.GCPLoadbalancers {
lb := &loadBalancer{
name: lbName,
hasForwardingRules: true,
hasBackendService: true,
hasHealthCheck: true,
}
c.loadbalancers = append(c.loadbalancers, lb)
}
}
func (c *Client) generateUID() (string, error) {

View file

@ -2,6 +2,7 @@ package client
import (
"errors"
"net/http"
"testing"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
@ -10,6 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"google.golang.org/api/googleapi"
)
func TestMain(m *testing.M) {
@ -41,16 +43,13 @@ func TestSetGetState(t *testing.T) {
GCPRegion: "region-id",
Name: "name",
UID: "uid",
BootstrapperHost: "ip3",
LoadBalancerIP: "ip5",
GCPNetwork: "net-id",
GCPSubnetwork: "subnet-id",
GCPFirewalls: []string{"fw-1", "fw-2"},
GCPWorkerInstanceTemplate: "temp-id",
GCPControlPlaneInstanceTemplate: "temp-id",
GCPServiceAccount: "service-account",
GCPBackendService: "backend-service-id",
GCPHealthCheck: "health-check-id",
GCPForwardingRule: "forwarding-rule-id",
GCPLoadbalancers: []string{"lb-1", "lb-2"},
}
t.Run("SetState", func(t *testing.T) {
@ -71,6 +70,13 @@ func TestSetGetState(t *testing.T) {
assert.Equal(state.GCPControlPlaneInstanceTemplate, client.controlPlaneTemplate)
assert.Equal(state.GCPWorkerInstanceTemplate, client.workerTemplate)
assert.Equal(state.GCPServiceAccount, client.serviceAccount)
assert.Equal(state.LoadBalancerIP, client.loadbalancerIP)
for _, lb := range client.loadbalancers {
assert.Contains(state.GCPLoadbalancers, lb.name)
assert.True(lb.hasBackendService)
assert.True(lb.hasHealthCheck)
assert.True(lb.hasForwardingRules)
}
})
t.Run("GetState", func(t *testing.T) {
@ -92,9 +98,11 @@ func TestSetGetState(t *testing.T) {
workerTemplate: state.GCPWorkerInstanceTemplate,
controlPlaneTemplate: state.GCPControlPlaneInstanceTemplate,
serviceAccount: state.GCPServiceAccount,
healthCheck: state.GCPHealthCheck,
backendService: state.GCPBackendService,
forwardingRule: state.GCPForwardingRule,
loadbalancerIP: state.LoadBalancerIP,
loadbalancerIPname: state.GCPLoadbalancerIPname,
}
for _, lbName := range state.GCPLoadbalancers {
client.loadbalancers = append(client.loadbalancers, &loadBalancer{name: lbName})
}
stat := client.GetState()
@ -141,3 +149,21 @@ func (c *someCloser) Close() error {
c.closed = true
return c.closeErr
}
func TestIsNotFoundError(t *testing.T) {
testCases := map[string]struct {
err error
result bool
}{
"not found error": {err: &googleapi.Error{Code: http.StatusNotFound}, result: true},
"nil error": {err: nil, result: false},
"other error": {err: errors.New("failed"), result: false},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
assert.Equal(tc.result, isNotFoundError(tc.err))
})
}
}

View file

@ -212,15 +212,21 @@ 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) {
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) {
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 {
func (c *iamClient) DeleteServiceAccount(ctx context.Context, req *adminpb.DeleteServiceAccountRequest,
opts ...gax.CallOption,
) error {
return c.IamClient.DeleteServiceAccount(ctx, req)
}
@ -232,10 +238,34 @@ 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) {
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) {
func (c *projectsClient) SetIamPolicy(ctx context.Context, req *iampb.SetIamPolicyRequest,
opts ...gax.CallOption,
) (*iampb.Policy, error) {
return c.ProjectsClient.SetIamPolicy(ctx, req)
}
type addressesClient struct {
*compute.AddressesClient
}
func (c *addressesClient) Insert(ctx context.Context, req *computepb.InsertAddressRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.AddressesClient.Insert(ctx, req)
}
func (c *addressesClient) Delete(ctx context.Context, req *computepb.DeleteAddressRequest,
opts ...gax.CallOption,
) (Operation, error) {
return c.AddressesClient.Delete(ctx, req)
}
func (c *addressesClient) Close() error {
return c.AddressesClient.Close()
}

View file

@ -9,6 +9,7 @@ import (
"github.com/edgelesssys/constellation/bootstrapper/role"
"github.com/edgelesssys/constellation/internal/cloud/cloudtypes"
"github.com/edgelesssys/constellation/internal/constants"
"google.golang.org/api/iterator"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
@ -76,8 +77,14 @@ func (c *Client) CreateInstances(ctx context.Context, input CreateInstancesInput
ops = []Operation{}
controlPlaneGroupInput := instanceGroupManagerInput{
Count: input.CountControlPlanes,
Name: strings.Join([]string{c.name, "control-plane", c.uid}, "-"),
Count: input.CountControlPlanes,
Name: strings.Join([]string{c.name, "control-plane", c.uid}, "-"),
NamedPorts: []*computepb.NamedPort{
{Name: proto.String("kubernetes"), Port: proto.Int32(constants.KubernetesPort)},
{Name: proto.String("debugd"), Port: proto.Int32(constants.DebugdPort)},
{Name: proto.String("bootstrapper"), Port: proto.Int32(constants.BootstrapperPort)},
{Name: proto.String("verify"), Port: proto.Int32(constants.VerifyServiceNodePortGRPC)},
},
Template: c.controlPlaneTemplate,
UID: c.uid,
Project: c.project,
@ -277,18 +284,20 @@ func (c *Client) getInstanceIPs(ctx context.Context, groupID string, list cloudt
}
type instanceGroupManagerInput struct {
Count int
Name string
Template string
Project string
Zone string
UID string
Count int
Name string
NamedPorts []*computepb.NamedPort
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),
NamedPorts: i.NamedPorts,
InstanceTemplate: proto.String("projects/" + i.Project + "/global/instanceTemplates/" + i.Template),
Name: proto.String(i.Name),
TargetSize: proto.Int32(int32(i.Count)),

View file

@ -0,0 +1,426 @@
package client
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/edgelesssys/constellation/internal/constants"
"go.uber.org/multierr"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
type loadBalancer struct {
name string
// For creation.
ip string
frontendPort int
backendPortName string
healthCheck computepb.HealthCheck_Type
label bool
// For resource management.
hasHealthCheck bool
hasBackendService bool
hasForwardingRules bool
}
// CreateLoadBalancers creates all necessary load balancers.
func (c *Client) CreateLoadBalancers(ctx context.Context) error {
if err := c.createIPAddr(ctx); err != nil {
return fmt.Errorf("creating load balancer IP address: %w", err)
}
//
// LoadBalancer definitions.
//
c.loadbalancers = append(c.loadbalancers, &loadBalancer{
name: c.name + "-" + "kube" + "-" + c.uid,
ip: c.loadbalancerIP,
frontendPort: constants.KubernetesPort,
backendPortName: "kubernetes",
healthCheck: computepb.HealthCheck_HTTPS,
label: true, // Label, so bootstrapper can find kube-apiserver.
})
c.loadbalancers = append(c.loadbalancers, &loadBalancer{
name: c.name + "-" + "boot" + "-" + c.uid,
ip: c.loadbalancerIPname,
frontendPort: constants.BootstrapperPort,
backendPortName: "bootstrapper",
healthCheck: computepb.HealthCheck_TCP,
})
c.loadbalancers = append(c.loadbalancers, &loadBalancer{
name: c.name + "-" + "verify" + "-" + c.uid,
ip: c.loadbalancerIPname,
frontendPort: constants.VerifyServiceNodePortGRPC,
backendPortName: "verify",
healthCheck: computepb.HealthCheck_TCP,
})
c.loadbalancers = append(c.loadbalancers, &loadBalancer{
name: c.name + "-" + "debugd" + "-" + c.uid,
ip: c.loadbalancerIPname,
frontendPort: constants.DebugdPort,
backendPortName: "debugd",
healthCheck: computepb.HealthCheck_TCP,
})
// Load balancer creation.
errC := make(chan error)
createLB := func(ctx context.Context, lb *loadBalancer) {
errC <- c.createLoadBalancer(ctx, lb)
}
for _, lb := range c.loadbalancers {
go createLB(ctx, lb)
}
var err error
for i := 0; i < len(c.loadbalancers); i++ {
err = multierr.Append(err, <-errC)
}
return err
}
// createLoadBalancer creates a load balancer.
func (c *Client) createLoadBalancer(ctx context.Context, lb *loadBalancer) error {
if err := c.createHealthCheck(ctx, lb); err != nil {
return fmt.Errorf("creating health checks: %w", err)
}
if err := c.createBackendService(ctx, lb); err != nil {
return fmt.Errorf("creating backend services: %w", err)
}
if err := c.createForwardingRules(ctx, lb); err != nil {
return fmt.Errorf("creating forwarding rules: %w", err)
}
return nil
}
func (c *Client) createHealthCheck(ctx context.Context, lb *loadBalancer) error {
req := &computepb.InsertRegionHealthCheckRequest{
Project: c.project,
Region: c.region,
HealthCheckResource: &computepb.HealthCheck{
Name: proto.String(lb.name),
Type: proto.String(computepb.HealthCheck_Type_name[int32(lb.healthCheck)]),
CheckIntervalSec: proto.Int32(1),
TimeoutSec: proto.Int32(1),
},
}
switch lb.healthCheck {
case computepb.HealthCheck_HTTPS:
req.HealthCheckResource.HttpsHealthCheck = newHealthCheckHTTPS(lb.frontendPort)
case computepb.HealthCheck_TCP:
req.HealthCheckResource.TcpHealthCheck = newHealthCheckTCP(lb.frontendPort)
}
resp, err := c.healthChecksAPI.Insert(ctx, req)
if err != nil {
return fmt.Errorf("inserting health check: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return fmt.Errorf("waiting for health check creation: %w", err)
}
lb.hasHealthCheck = true
return nil
}
func (c *Client) createBackendService(ctx context.Context, lb *loadBalancer) error {
req := &computepb.InsertRegionBackendServiceRequest{
Project: c.project,
Region: c.region,
BackendServiceResource: &computepb.BackendService{
Name: proto.String(lb.name),
Protocol: proto.String(computepb.BackendService_Protocol_name[int32(computepb.BackendService_TCP)]),
LoadBalancingScheme: proto.String(computepb.BackendService_LoadBalancingScheme_name[int32(computepb.BackendService_EXTERNAL)]),
HealthChecks: []string{"https://www.googleapis.com/compute/v1/projects/" + c.project + "/regions/" + c.region + "/healthChecks/" + lb.name},
PortName: proto.String(lb.backendPortName),
Backends: []*computepb.Backend{
{
BalancingMode: proto.String(computepb.Backend_BalancingMode_name[int32(computepb.Backend_CONNECTION)]),
Group: proto.String("https://www.googleapis.com/compute/v1/projects/" + c.project + "/zones/" + c.zone + "/instanceGroups/" + c.controlPlaneInstanceGroup),
},
},
},
}
resp, err := c.backendServicesAPI.Insert(ctx, req)
if err != nil {
return fmt.Errorf("inserting backend services: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return fmt.Errorf("waiting for backend services creation: %w", err)
}
lb.hasBackendService = true
return nil
}
func (c *Client) createForwardingRules(ctx context.Context, lb *loadBalancer) error {
req := &computepb.InsertForwardingRuleRequest{
Project: c.project,
Region: c.region,
ForwardingRuleResource: &computepb.ForwardingRule{
Name: proto.String(lb.name),
IPAddress: proto.String("https://www.googleapis.com/compute/v1/projects/" + c.project + "/regions/" + c.region + "/addresses/" + c.loadbalancerIPname),
IPProtocol: proto.String(computepb.ForwardingRule_IPProtocolEnum_name[int32(computepb.ForwardingRule_TCP)]),
LoadBalancingScheme: proto.String(computepb.ForwardingRule_LoadBalancingScheme_name[int32(computepb.ForwardingRule_EXTERNAL)]),
Ports: []string{strconv.Itoa(lb.frontendPort)},
BackendService: proto.String("https://www.googleapis.com/compute/v1/projects/" + c.project + "/regions/" + c.region + "/backendServices/" + lb.name),
},
}
resp, err := c.forwardingRulesAPI.Insert(ctx, req)
if err != nil {
return fmt.Errorf("inserting forwarding rules: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
lb.hasForwardingRules = true
if lb.label {
return c.labelLoadBalancer(ctx, lb.name)
}
return nil
}
// labelLoadBalancer labels a load balancer (its forwarding rules) so that it can be found by applications in the cluster.
func (c *Client) labelLoadBalancer(ctx context.Context, name string) error {
forwardingRule, err := c.forwardingRulesAPI.Get(ctx, &computepb.GetForwardingRuleRequest{
Project: c.project,
Region: c.region,
ForwardingRule: name,
})
if err != nil {
return fmt.Errorf("getting forwarding rule: %w", err)
}
if forwardingRule.LabelFingerprint == nil {
return fmt.Errorf("forwarding rule %s has no label fingerprint", name)
}
resp, err := c.forwardingRulesAPI.SetLabels(ctx, &computepb.SetLabelsForwardingRuleRequest{
Project: c.project,
Region: c.region,
Resource: name,
RegionSetLabelsRequestResource: &computepb.RegionSetLabelsRequest{
Labels: map[string]string{"constellation-uid": c.uid},
LabelFingerprint: forwardingRule.LabelFingerprint,
},
})
if err != nil {
return fmt.Errorf("setting forwarding rule labels: %w", err)
}
return c.waitForOperations(ctx, []Operation{resp})
}
// TerminateLoadBalancers terminates all load balancers.
func (c *Client) TerminateLoadBalancers(ctx context.Context) error {
errC := make(chan error)
terminateLB := func(ctx context.Context, lb *loadBalancer) {
errC <- c.terminateLoadBalancer(ctx, lb)
}
for _, lb := range c.loadbalancers {
go terminateLB(ctx, lb)
}
var err error
for i := 0; i < len(c.loadbalancers); i++ {
err = multierr.Append(err, <-errC)
}
if err != nil && !isNotFoundError(err) {
return err
}
if err := c.deleteIPAddr(ctx); err != nil {
return fmt.Errorf("deleting load balancer IP address: %w", err)
}
c.loadbalancers = nil
return nil
}
// terminateLoadBalancer removes the load balancer and its associated resources.
func (c *Client) terminateLoadBalancer(ctx context.Context, lb *loadBalancer) error {
if lb == nil {
return nil
}
if lb.name == "" {
return errors.New("load balancer name is empty")
}
if lb.hasForwardingRules {
if err := c.terminateForwadingRules(ctx, lb); err != nil {
return fmt.Errorf("terminating forwarding rules: %w", err)
}
}
if lb.hasBackendService {
if err := c.terminateBackendService(ctx, lb); err != nil {
return fmt.Errorf("terminating backend services: %w", err)
}
}
if lb.hasHealthCheck {
if err := c.terminateHealthCheck(ctx, lb); err != nil {
return fmt.Errorf("terminating health checks: %w", err)
}
}
lb.name = ""
return nil
}
func (c *Client) terminateForwadingRules(ctx context.Context, lb *loadBalancer) error {
resp, err := c.forwardingRulesAPI.Delete(ctx, &computepb.DeleteForwardingRuleRequest{
Project: c.project,
Region: c.region,
ForwardingRule: lb.name,
})
if isNotFoundError(err) {
lb.hasForwardingRules = false
return nil
}
if err != nil {
return fmt.Errorf("deleting forwarding rules: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
lb.hasForwardingRules = false
return nil
}
func (c *Client) terminateBackendService(ctx context.Context, lb *loadBalancer) error {
resp, err := c.backendServicesAPI.Delete(ctx, &computepb.DeleteRegionBackendServiceRequest{
Project: c.project,
Region: c.region,
BackendService: lb.name,
})
if isNotFoundError(err) {
lb.hasBackendService = false
return nil
}
if err != nil {
return fmt.Errorf("deleting backend services: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
lb.hasBackendService = false
return nil
}
func (c *Client) terminateHealthCheck(ctx context.Context, lb *loadBalancer) error {
resp, err := c.healthChecksAPI.Delete(ctx, &computepb.DeleteRegionHealthCheckRequest{
Project: c.project,
Region: c.region,
HealthCheck: lb.name,
})
if isNotFoundError(err) {
lb.hasHealthCheck = false
return nil
}
if err != nil {
return fmt.Errorf("deleting health checks: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
lb.hasHealthCheck = false
return nil
}
func (c *Client) createIPAddr(ctx context.Context) error {
ipName := c.name + "-" + c.uid
insertReq := &computepb.InsertAddressRequest{
Project: c.project,
Region: c.region,
AddressResource: &computepb.Address{
Name: proto.String(ipName),
},
}
op, err := c.addressesAPI.Insert(ctx, insertReq)
if err != nil {
return fmt.Errorf("inserting address: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{op}); err != nil {
return err
}
c.loadbalancerIPname = c.name + "-" + c.uid
getReq := &computepb.GetAddressRequest{
Project: c.project,
Region: c.region,
Address: c.loadbalancerIPname,
}
addr, err := c.addressesAPI.Get(ctx, getReq)
if err != nil {
return fmt.Errorf("getting address: %w", err)
}
if addr.Address == nil {
return fmt.Errorf("address response without address: %q", addr)
}
c.loadbalancerIP = *addr.Address
return nil
}
func (c *Client) deleteIPAddr(ctx context.Context) error {
if c.loadbalancerIPname == "" {
return nil
}
req := &computepb.DeleteAddressRequest{
Project: c.project,
Region: c.region,
Address: c.loadbalancerIPname,
}
op, err := c.addressesAPI.Delete(ctx, req)
if isNotFoundError(err) {
c.loadbalancerIPname = ""
return nil
}
if err != nil {
return fmt.Errorf("deleting address: %w", err)
}
if err := c.waitForOperations(ctx, []Operation{op}); err != nil {
return err
}
c.loadbalancerIPname = ""
return nil
}
func newHealthCheckHTTPS(port int) *computepb.HTTPSHealthCheck {
return &computepb.HTTPSHealthCheck{
Host: proto.String(""),
Port: proto.Int32(int32(port)),
RequestPath: proto.String("/readyz"),
}
}
func newHealthCheckTCP(port int) *computepb.TCPHealthCheck {
return &computepb.TCPHealthCheck{
Port: proto.Int32(int32(port)),
}
}

View file

@ -0,0 +1,628 @@
package client
import (
"context"
"errors"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"google.golang.org/api/googleapi"
"google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
func TestCreateLoadBalancers(t *testing.T) {
someErr := errors.New("failed")
forwardingRule := &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}
testCases := map[string]struct {
addrAPI addressesAPI
healthAPI healthChecksAPI
backendAPI backendServicesAPI
forwardAPI forwardingRulesAPI
opRegAPI operationRegionAPI
wantErr bool
}{
"successful create": {
addrAPI: &stubAddressesAPI{getAddr: proto.String("192.0.2.1")},
healthAPI: &stubHealthChecksAPI{},
backendAPI: &stubBackendServicesAPI{},
forwardAPI: &stubForwardingRulesAPI{forwardingRule: forwardingRule},
opRegAPI: stubOperationRegionAPI{},
},
"createIPAddr fails": {
addrAPI: &stubAddressesAPI{insertErr: someErr},
healthAPI: &stubHealthChecksAPI{},
backendAPI: &stubBackendServicesAPI{},
forwardAPI: &stubForwardingRulesAPI{forwardingRule: forwardingRule},
opRegAPI: stubOperationRegionAPI{},
wantErr: true,
},
"createLB fails": {
addrAPI: &stubAddressesAPI{},
healthAPI: &stubHealthChecksAPI{},
backendAPI: &stubBackendServicesAPI{insertErr: someErr},
forwardAPI: &stubForwardingRulesAPI{forwardingRule: forwardingRule},
opRegAPI: stubOperationRegionAPI{},
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",
addressesAPI: tc.addrAPI,
healthChecksAPI: tc.healthAPI,
backendServicesAPI: tc.backendAPI,
forwardingRulesAPI: tc.forwardAPI,
operationRegionAPI: tc.opRegAPI,
}
err := client.CreateLoadBalancers(ctx)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.NotEmpty(client.loadbalancerIPname)
assert.Equal(4, len(client.loadbalancers))
}
})
}
}
func TestCreateLoadBalancer(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
operationRegionAPI operationRegionAPI
healthChecksAPI healthChecksAPI
backendServicesAPI backendServicesAPI
forwardingRulesAPI forwardingRulesAPI
wantErr bool
wantLB *loadBalancer
}{
"successful create": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}},
operationRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: true,
},
},
"successful create with label": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}},
operationRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
label: true,
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: true,
},
},
"CreateLoadBalancer fails when getting forwarding rule": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{getErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
label: true,
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: true,
},
},
"CreateLoadBalancer fails when label fingerprint is missing": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{}},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
label: true,
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: true,
},
},
"CreateLoadBalancer fails when creating health check": {
healthChecksAPI: stubHealthChecksAPI{insertErr: someErr},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
hasHealthCheck: false,
hasBackendService: false,
hasForwardingRules: false,
},
},
"CreateLoadBalancer fails when creating backend service": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{insertErr: someErr},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
hasHealthCheck: true,
hasBackendService: false,
hasForwardingRules: false,
},
},
"CreateLoadBalancer fails when creating forwarding rule": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{insertErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: false,
},
},
"CreateLoadBalancer fails when waiting on operation": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}},
operationRegionAPI: stubOperationRegionAPI{waitErr: someErr},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
frontendPort: 1234,
backendPortName: "testport",
hasHealthCheck: false,
hasBackendService: false,
hasForwardingRules: false,
},
},
}
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",
backendServicesAPI: tc.backendServicesAPI,
forwardingRulesAPI: tc.forwardingRulesAPI,
healthChecksAPI: tc.healthChecksAPI,
operationRegionAPI: tc.operationRegionAPI,
}
lb := &loadBalancer{
name: tc.wantLB.name,
frontendPort: tc.wantLB.frontendPort,
backendPortName: tc.wantLB.backendPortName,
label: tc.wantLB.label,
}
err := client.createLoadBalancer(ctx, lb)
if tc.wantErr {
assert.Error(err)
assert.Equal(tc.wantLB, lb)
} else {
assert.NoError(err)
assert.Equal(tc.wantLB, lb)
}
})
}
}
func TestTerminateLoadbalancers(t *testing.T) {
someErr := errors.New("failed")
newRunningLB := func() *loadBalancer {
return &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: true,
}
}
testCases := map[string]struct {
addrAPI addressesAPI
healthAPI healthChecksAPI
backendAPI backendServicesAPI
forwardAPI forwardingRulesAPI
opRegionAPI operationRegionAPI
wantErr bool
}{
"successful terminate": {
addrAPI: &stubAddressesAPI{},
healthAPI: &stubHealthChecksAPI{},
backendAPI: &stubBackendServicesAPI{},
forwardAPI: &stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
},
"deleteIPAddr fails": {
addrAPI: &stubAddressesAPI{deleteErr: someErr},
healthAPI: &stubHealthChecksAPI{},
backendAPI: &stubBackendServicesAPI{},
forwardAPI: &stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"deleteLB fails": {
addrAPI: &stubAddressesAPI{},
healthAPI: &stubHealthChecksAPI{},
backendAPI: &stubBackendServicesAPI{deleteErr: someErr},
forwardAPI: &stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
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",
addressesAPI: tc.addrAPI,
healthChecksAPI: tc.healthAPI,
backendServicesAPI: tc.backendAPI,
forwardingRulesAPI: tc.forwardAPI,
operationRegionAPI: tc.opRegionAPI,
loadbalancerIPname: "loadbalancerIPid",
loadbalancers: []*loadBalancer{
newRunningLB(),
newRunningLB(),
newRunningLB(),
},
}
err := client.TerminateLoadBalancers(ctx)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Empty(client.loadbalancerIPname)
assert.Nil(client.loadbalancers)
}
})
}
}
func TestTerminateLoadBalancer(t *testing.T) {
someErr := errors.New("failed")
notFoundErr := &googleapi.Error{Code: http.StatusNotFound}
newRunningLB := func() *loadBalancer {
return &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: true,
}
}
testCases := map[string]struct {
lb *loadBalancer
opRegionAPI operationRegionAPI
healthChecksAPI healthChecksAPI
backendServicesAPI backendServicesAPI
forwardingRulesAPI forwardingRulesAPI
wantErr bool
wantLB *loadBalancer
}{
"successful terminate": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{},
},
"terminate partially created loadbalancer": {
lb: &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: false,
},
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{deleteErr: someErr},
opRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{},
},
"terminate partially created loadbalancer 2": {
lb: &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: false,
hasForwardingRules: false,
},
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{deleteErr: someErr},
forwardingRulesAPI: stubForwardingRulesAPI{deleteErr: someErr},
opRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{},
},
"no-op for nil loadbalancer": {
lb: nil,
},
"health check not found": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{deleteErr: notFoundErr},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{},
},
"backend service not found": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{deleteErr: notFoundErr},
forwardingRulesAPI: stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{},
},
"forwarding rules not found": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{deleteErr: notFoundErr},
opRegionAPI: stubOperationRegionAPI{},
wantLB: &loadBalancer{},
},
"fails for loadbalancer without name": {
lb: &loadBalancer{},
wantErr: true,
wantLB: &loadBalancer{},
},
"fails when deleting health check": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{deleteErr: someErr},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: false,
hasForwardingRules: false,
},
},
"fails when deleting backend service": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{deleteErr: someErr},
forwardingRulesAPI: stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: false,
},
},
"fails when deleting forwarding rule": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{deleteErr: someErr},
opRegionAPI: stubOperationRegionAPI{},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: true,
},
},
"fails when waiting on operation": {
lb: newRunningLB(),
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
opRegionAPI: stubOperationRegionAPI{waitErr: someErr},
wantErr: true,
wantLB: &loadBalancer{
name: "name",
hasHealthCheck: true,
hasBackendService: true,
hasForwardingRules: 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",
backendServicesAPI: tc.backendServicesAPI,
forwardingRulesAPI: tc.forwardingRulesAPI,
healthChecksAPI: tc.healthChecksAPI,
operationRegionAPI: tc.opRegionAPI,
}
err := client.terminateLoadBalancer(ctx, tc.lb)
if tc.wantErr {
assert.Error(err)
assert.Equal(tc.wantLB, tc.lb)
} else {
assert.NoError(err)
assert.Equal(tc.wantLB, tc.lb)
}
})
}
}
func TestCreateIPAddr(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
addrAPI addressesAPI
opAPI operationRegionAPI
wantErr bool
}{
"successful create": {
addrAPI: stubAddressesAPI{getAddr: proto.String("test-ip")},
opAPI: stubOperationRegionAPI{},
},
"insert fails": {
addrAPI: stubAddressesAPI{insertErr: someErr},
opAPI: stubOperationRegionAPI{},
wantErr: true,
},
"get fails": {
addrAPI: stubAddressesAPI{getErr: someErr},
opAPI: stubOperationRegionAPI{},
wantErr: true,
},
"get address nil": {
addrAPI: stubAddressesAPI{getAddr: nil},
opAPI: stubOperationRegionAPI{},
wantErr: true,
},
"wait fails": {
addrAPI: stubAddressesAPI{},
opAPI: stubOperationRegionAPI{waitErr: 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",
addressesAPI: tc.addrAPI,
operationRegionAPI: tc.opAPI,
}
err := client.createIPAddr(ctx)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal("test-ip", client.loadbalancerIP)
assert.Equal("name-uid", client.loadbalancerIPname)
}
})
}
}
func TestDeleteIPAddr(t *testing.T) {
someErr := errors.New("failed")
notFoundErr := &googleapi.Error{Code: http.StatusNotFound}
testCases := map[string]struct {
addrAPI addressesAPI
opAPI operationRegionAPI
addrID string
wantErr bool
}{
"successful delete": {
addrAPI: stubAddressesAPI{},
opAPI: stubOperationRegionAPI{},
addrID: "name",
},
"not found": {
addrAPI: stubAddressesAPI{deleteErr: notFoundErr},
opAPI: stubOperationRegionAPI{},
addrID: "name",
},
"empty is no-op": {
addrAPI: stubAddressesAPI{},
opAPI: stubOperationRegionAPI{},
},
"delete fails": {
addrAPI: stubAddressesAPI{deleteErr: someErr},
opAPI: stubOperationRegionAPI{},
addrID: "name",
wantErr: true,
},
"wait fails": {
addrAPI: stubAddressesAPI{},
opAPI: stubOperationRegionAPI{waitErr: someErr},
addrID: "name",
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",
addressesAPI: tc.addrAPI,
operationRegionAPI: tc.opAPI,
loadbalancerIPname: tc.addrID,
}
err := client.deleteIPAddr(ctx)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Empty(client.loadbalancerIPname)
}
})
}
}

View file

@ -3,11 +3,8 @@ package client
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/edgelesssys/constellation/internal/cloud/cloudtypes"
"github.com/edgelesssys/constellation/internal/constants"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
@ -226,148 +223,3 @@ func (c *Client) terminateSubnet(ctx context.Context) error {
c.subnetwork = ""
return nil
}
// CreateLoadBalancer creates a load balancer.
func (c *Client) CreateLoadBalancer(ctx context.Context) error {
c.healthCheck = c.name + "-" + c.uid
resp, err := c.healthChecksAPI.Insert(ctx, &computepb.InsertRegionHealthCheckRequest{
Project: c.project,
Region: c.region,
HealthCheckResource: &computepb.HealthCheck{
Name: proto.String(c.healthCheck),
Type: proto.String(computepb.HealthCheck_Type_name[int32(computepb.HealthCheck_HTTPS)]),
CheckIntervalSec: proto.Int32(1),
TimeoutSec: proto.Int32(1),
HttpsHealthCheck: &computepb.HTTPSHealthCheck{
Host: proto.String(""),
Port: proto.Int32(6443),
RequestPath: proto.String("/readyz"),
},
},
})
if err != nil {
return err
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
c.backendService = c.name + "-" + c.uid
resp, err = c.backendServicesAPI.Insert(ctx, &computepb.InsertRegionBackendServiceRequest{
Project: c.project,
Region: c.region,
BackendServiceResource: &computepb.BackendService{
Name: proto.String(c.backendService),
Protocol: proto.String(computepb.BackendService_Protocol_name[int32(computepb.BackendService_TCP)]),
LoadBalancingScheme: proto.String(computepb.BackendService_LoadBalancingScheme_name[int32(computepb.BackendService_EXTERNAL)]),
TimeoutSec: proto.Int32(10),
HealthChecks: []string{"https://www.googleapis.com/compute/v1/projects/" + c.project + "/regions/" + c.region + "/healthChecks/" + c.healthCheck},
Backends: []*computepb.Backend{
{
BalancingMode: proto.String(computepb.Backend_BalancingMode_name[int32(computepb.Backend_CONNECTION)]),
Group: proto.String("https://www.googleapis.com/compute/v1/projects/" + c.project + "/zones/" + c.zone + "/instanceGroups/" + c.controlPlaneInstanceGroup),
},
},
},
})
if err != nil {
return err
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
c.forwardingRule = c.name + "-" + c.uid
resp, err = c.forwardingRulesAPI.Insert(ctx, &computepb.InsertForwardingRuleRequest{
Project: c.project,
Region: c.region,
ForwardingRuleResource: &computepb.ForwardingRule{
Name: proto.String(c.forwardingRule),
IPProtocol: proto.String(computepb.ForwardingRule_IPProtocolEnum_name[int32(computepb.ForwardingRule_TCP)]),
LoadBalancingScheme: proto.String(computepb.ForwardingRule_LoadBalancingScheme_name[int32(computepb.ForwardingRule_EXTERNAL)]),
Ports: []string{"6443", "9000", strconv.Itoa(constants.VerifyServiceNodePortGRPC)},
BackendService: proto.String("https://www.googleapis.com/compute/v1/projects/" + c.project + "/regions/" + c.region + "/backendServices/" + c.backendService),
},
})
if err != nil {
return err
}
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
forwardingRule, err := c.forwardingRulesAPI.Get(ctx, &computepb.GetForwardingRuleRequest{
Project: c.project,
Region: c.region,
ForwardingRule: c.forwardingRule,
})
if err != nil {
return err
}
if forwardingRule.LabelFingerprint == nil {
return fmt.Errorf("forwarding rule %s has no label fingerprint", c.forwardingRule)
}
resp, err = c.forwardingRulesAPI.SetLabels(ctx, &computepb.SetLabelsForwardingRuleRequest{
Project: c.project,
Region: c.region,
Resource: c.forwardingRule,
RegionSetLabelsRequestResource: &computepb.RegionSetLabelsRequest{
Labels: map[string]string{"constellation-uid": c.uid},
LabelFingerprint: forwardingRule.LabelFingerprint,
},
})
if err != nil {
return err
}
return c.waitForOperations(ctx, []Operation{resp})
}
// TerminateLoadBalancer removes the load balancer and its associated resources.
func (c *Client) TerminateLoadBalancer(ctx context.Context) error {
resp, err := c.forwardingRulesAPI.Delete(ctx, &computepb.DeleteForwardingRuleRequest{
Project: c.project,
Region: c.region,
ForwardingRule: c.forwardingRule,
})
if err != nil && !isNotFoundError(err) {
return err
}
if err == nil {
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
}
resp, err = c.backendServicesAPI.Delete(ctx, &computepb.DeleteRegionBackendServiceRequest{
Project: c.project,
Region: c.region,
BackendService: c.backendService,
})
if err != nil && !isNotFoundError(err) {
return err
}
if err == nil {
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
}
resp, err = c.healthChecksAPI.Delete(ctx, &computepb.DeleteRegionHealthCheckRequest{
Project: c.project,
Region: c.region,
HealthCheck: c.healthCheck,
})
if err != nil && !isNotFoundError(err) {
return err
}
if err == nil {
if err := c.waitForOperations(ctx, []Operation{resp}); err != nil {
return err
}
}
return nil
}

View file

@ -9,8 +9,6 @@ import (
"github.com/edgelesssys/constellation/internal/cloud/cloudtypes"
"github.com/stretchr/testify/assert"
"google.golang.org/api/googleapi"
"google.golang.org/genproto/googleapis/cloud/compute/v1"
"google.golang.org/protobuf/proto"
)
func TestCreateVPCs(t *testing.T) {
@ -351,183 +349,3 @@ func TestTerminateFirewall(t *testing.T) {
})
}
}
func TestCreateLoadBalancer(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
operationRegionAPI operationRegionAPI
healthChecksAPI healthChecksAPI
backendServicesAPI backendServicesAPI
forwardingRulesAPI forwardingRulesAPI
wantErr bool
}{
"successful create": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}},
operationRegionAPI: stubOperationRegionAPI{},
},
"CreateLoadBalancer fails when getting forwarding rule": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{getErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"CreateLoadBalancer fails when label fingerprint is missing": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{}},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"CreateLoadBalancer fails when creating health check": {
healthChecksAPI: stubHealthChecksAPI{insertErr: someErr},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"CreateLoadBalancer fails when creating backend service": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{insertErr: someErr},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"CreateLoadBalancer fails when creating forwarding rule": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{insertErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"CreateLoadBalancer fails when waiting on operation": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{forwardingRule: &compute.ForwardingRule{LabelFingerprint: proto.String("fingerprint")}},
operationRegionAPI: stubOperationRegionAPI{waitErr: 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",
backendServicesAPI: tc.backendServicesAPI,
forwardingRulesAPI: tc.forwardingRulesAPI,
healthChecksAPI: tc.healthChecksAPI,
operationRegionAPI: tc.operationRegionAPI,
}
if tc.wantErr {
assert.Error(client.CreateLoadBalancer(ctx))
} else {
assert.NoError(client.CreateLoadBalancer(ctx))
assert.NotEmpty(client.healthCheck)
assert.NotEmpty(client.backendService)
assert.NotEmpty(client.forwardingRule)
}
})
}
}
func TestTerminateLoadBalancer(t *testing.T) {
someErr := errors.New("failed")
notFoundErr := &googleapi.Error{Code: http.StatusNotFound}
testCases := map[string]struct {
operationRegionAPI operationRegionAPI
healthChecksAPI healthChecksAPI
backendServicesAPI backendServicesAPI
forwardingRulesAPI forwardingRulesAPI
wantErr bool
}{
"successful terminate": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{},
},
"successful terminate when health check not found": {
healthChecksAPI: stubHealthChecksAPI{deleteErr: notFoundErr},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{},
},
"successful terminate when backend service not found": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{deleteErr: notFoundErr},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{},
},
"successful terminate when forwarding rule not found": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{deleteErr: notFoundErr},
operationRegionAPI: stubOperationRegionAPI{},
},
"TerminateLoadBalancer fails when deleting health check": {
healthChecksAPI: stubHealthChecksAPI{deleteErr: someErr},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"TerminateLoadBalancer fails when deleting backend service": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{deleteErr: someErr},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"TerminateLoadBalancer fails when deleting forwarding rule": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{deleteErr: someErr},
operationRegionAPI: stubOperationRegionAPI{},
wantErr: true,
},
"TerminateLoadBalancer fails when waiting on operation": {
healthChecksAPI: stubHealthChecksAPI{},
backendServicesAPI: stubBackendServicesAPI{},
forwardingRulesAPI: stubForwardingRulesAPI{},
operationRegionAPI: stubOperationRegionAPI{waitErr: 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",
backendServicesAPI: tc.backendServicesAPI,
forwardingRulesAPI: tc.forwardingRulesAPI,
healthChecksAPI: tc.healthChecksAPI,
operationRegionAPI: tc.operationRegionAPI,
}
if tc.wantErr {
assert.Error(client.TerminateLoadBalancer(ctx))
} else {
assert.NoError(client.TerminateLoadBalancer(ctx))
assert.Empty(client.healthCheck)
assert.Empty(client.backendService)
assert.Empty(client.forwardingRule)
}
})
}
}

View file

@ -37,6 +37,9 @@ func (c *Client) waitForGlobalOperation(ctx context.Context, op Operation) error
if err := ctx.Err(); err != nil {
return err
}
if op.Proto().Name == nil {
return errors.New("operation name is nil")
}
waitReq := &computepb.WaitGlobalOperationRequest{
Operation: *op.Proto().Name,
Project: c.project,
@ -59,6 +62,9 @@ func (c *Client) waitForZoneOperation(ctx context.Context, op Operation) error {
if err := ctx.Err(); err != nil {
return err
}
if op.Proto().Name == nil {
return errors.New("operation name is nil")
}
waitReq := &computepb.WaitZoneOperationRequest{
Operation: *op.Proto().Name,
Project: c.project,
@ -82,6 +88,9 @@ func (c *Client) waitForRegionOperation(ctx context.Context, op Operation) error
if err := ctx.Err(); err != nil {
return err
}
if op.Proto().Name == nil {
return errors.New("operation name is nil")
}
waitReq := &computepb.WaitRegionOperationRequest{
Operation: *op.Proto().Name,
Project: c.project,