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/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/internal/cloud/cloudtypes" "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 forwardingRulesAPI backendServicesAPI healthChecksAPI instanceTemplateAPI instanceGroupManagersAPI iamAPI projectsAPI nodes cloudtypes.Instances coordinators cloudtypes.Instances nodesInstanceGroup string coordinatorInstanceGroup string coordinatorTemplate string nodeTemplate string network string subnetwork string secondarySubnetworkRange string firewalls []string name string project string uid string zone string region string serviceAccount string // loadbalancer healthCheck string backendService string forwardingRule 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, subnetAPI) forwardingRulesAPI, err := compute.NewForwardingRulesRESTClient(ctx) if err != nil { _ = closeAll(closers) return nil, err } closers = append(closers, forwardingRulesAPI) backendServicesAPI, err := compute.NewRegionBackendServicesRESTClient(ctx) if err != nil { _ = closeAll(closers) return nil, err } closers = append(closers, backendServicesAPI) targetPoolsAPI, err := compute.NewTargetPoolsRESTClient(ctx) if err != nil { _ = closeAll(closers) return nil, err } closers = append(closers, targetPoolsAPI) healthChecksAPI, err := compute.NewRegionHealthChecksRESTClient(ctx) if err != nil { _ = closeAll(closers) return nil, err } closers = append(closers, healthChecksAPI) 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}, forwardingRulesAPI: &forwardingRulesClient{forwardingRulesAPI}, backendServicesAPI: &backendServicesClient{backendServicesAPI}, healthChecksAPI: &healthChecksClient{healthChecksAPI}, instanceTemplateAPI: &instanceTemplateClient{templAPI}, instanceGroupManagersAPI: &instanceGroupManagersClient{groupAPI}, iamAPI: &iamClient{iamAPI}, projectsAPI: &projectsClient{projectsAPI}, nodes: make(cloudtypes.Instances), coordinators: make(cloudtypes.Instances), }, nil } // NewInitialized creates an initialized client. func NewInitialized(ctx context.Context, project, zone, region, name string) (*Client, error) { client, err := NewFromDefault(ctx) if err != nil { return nil, err } err = client.init(project, zone, region, name) return client, err } // Close closes the client's connection. func (c *Client) Close() error { closers := []closer{ c.instanceAPI, c.operationRegionAPI, c.operationZoneAPI, c.operationGlobalAPI, c.networksAPI, c.subnetworksAPI, c.firewallsAPI, c.forwardingRulesAPI, c.backendServicesAPI, c.healthChecksAPI, c.instanceTemplateAPI, c.instanceGroupManagersAPI, c.iamAPI, c.projectsAPI, } 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 if c.healthCheck == "" { return state.ConstellationState{}, errors.New("client has no health check") } stat.GCPHealthCheck = c.healthCheck if c.backendService == "" { return state.ConstellationState{}, errors.New("client has no backend service") } stat.GCPBackendService = c.backendService if c.forwardingRule == "" { return state.ConstellationState{}, errors.New("client has no forwarding rule") } stat.GCPForwardingRule = c.forwardingRule // 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 if stat.GCPHealthCheck == "" { return errors.New("state has no health check") } c.healthCheck = stat.GCPHealthCheck if stat.GCPBackendService == "" { return errors.New("state has no backend service") } c.backendService = stat.GCPBackendService if stat.GCPForwardingRule == "" { return errors.New("state has no forwarding rule") } c.forwardingRule = stat.GCPForwardingRule // 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 }