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.buildResourceName("kube"), 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.buildResourceName("boot"), ip: c.loadbalancerIPname, frontendPort: constants.BootstrapperPort, backendPortName: "bootstrapper", healthCheck: computepb.HealthCheck_TCP, }) c.loadbalancers = append(c.loadbalancers, &loadBalancer{ name: c.buildResourceName("verify"), ip: c.loadbalancerIPname, frontendPort: constants.VerifyServiceNodePortGRPC, backendPortName: "verify", healthCheck: computepb.HealthCheck_TCP, }) c.loadbalancers = append(c.loadbalancers, &loadBalancer{ name: c.buildResourceName("debugd"), 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{c.resourceURI(scopeRegion, "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(c.resourceURI(scopeZone, "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(c.resourceURI(scopeRegion, "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(c.resourceURI(scopeRegion, "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.buildResourceName() 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 = ipName 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)), } }