pkg/validators/cloud.go (196 lines of code) (raw):

// Copyright 2023 "Google LLC" // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package validators import ( "context" "errors" "fmt" "hpc-toolkit/pkg/config" "strings" "golang.org/x/exp/maps" compute "google.golang.org/api/compute/v1" "google.golang.org/api/googleapi" "google.golang.org/api/option" serviceusage "google.golang.org/api/serviceusage/v1" ) func getErrorReason(err googleapi.Error) (string, map[string]interface{}) { for _, d := range err.Details { m, ok := d.(map[string]interface{}) if !ok { continue } if reason, ok := m["reason"].(string); ok { return reason, m["metadata"].(map[string]interface{}) } } return "", nil } func newDisabledServiceError(title string, name string, pid string) error { return config.HintError{ Hint: fmt.Sprintf("%s can be enabled at https://console.cloud.google.com/apis/library/%s?project=%s", title, name, pid), Err: fmt.Errorf("%s service is disabled in project %s", title, pid)} } func handleServiceUsageError(err error, pid string) error { if err == nil { return nil } var herr *googleapi.Error if !errors.As(err, &herr) { return fmt.Errorf("unhandled error: %s", err) } reason, metadata := getErrorReason(*herr) switch reason { case "SERVICE_DISABLED": return newDisabledServiceError("Service Usage API", "serviceusage.googleapis.com", pid) case "SERVICE_CONFIG_NOT_FOUND_OR_PERMISSION_DENIED": return fmt.Errorf("service %s does not exist in project %s", metadata["services"], pid) case "USER_PROJECT_DENIED": return projectError(pid) case "SU_MISSING_NAMES": return nil // occurs if API list is empty and 0 APIs to validate } return fmt.Errorf("unhandled error: %s", herr) } // TestApisEnabled tests whether APIs are enabled in given project func TestApisEnabled(projectID string, requiredAPIs []string) error { // can return immediately if there are 0 APIs to test if len(requiredAPIs) == 0 { return nil } ctx := context.Background() s, err := serviceusage.NewService(ctx, option.WithQuotaProject(projectID)) if err != nil { return handleClientError(err) } prefix := "projects/" + projectID var serviceNames []string for _, api := range requiredAPIs { serviceNames = append(serviceNames, prefix+"/services/"+api) } resp, err := s.Services.BatchGet(prefix).Names(serviceNames...).Do() if err != nil { return handleServiceUsageError(err, projectID) } errs := config.Errors{} for _, service := range resp.Services { if service.State == "DISABLED" { errs.Add(newDisabledServiceError(service.Config.Title, service.Config.Name, projectID)) } } return errs.OrNil() } // TestProjectExists whether projectID exists / is accessible with credentials func TestProjectExists(projectID string) error { ctx := context.Background() s, err := compute.NewService(ctx) if err != nil { err = handleClientError(err) return err } _, err = s.Projects.Get(projectID).Fields().Do() if err != nil { if strings.Contains(err.Error(), "Compute Engine API has not been used in project") { return newDisabledServiceError("Compute Engine API", "compute.googleapis.com", projectID) } return projectError(projectID) } return nil } func getRegion(projectID string, region string) (*compute.Region, error) { ctx := context.Background() s, err := compute.NewService(ctx) if err != nil { err = handleClientError(err) return nil, err } return s.Regions.Get(projectID, region).Do() } // TestRegionExists whether region exists / is accessible with credentials func TestRegionExists(projectID string, region string) error { _, err := getRegion(projectID, region) if err != nil { return fmt.Errorf(regionError, region, projectID) } return nil } func getZone(projectID string, zone string) (*compute.Zone, error) { ctx := context.Background() s, err := compute.NewService(ctx) if err != nil { err = handleClientError(err) return nil, err } return s.Zones.Get(projectID, zone).Do() } // TestZoneExists whether zone exists / is accessible with credentials func TestZoneExists(projectID string, zone string) error { _, err := getZone(projectID, zone) if err != nil { return fmt.Errorf(zoneError, zone, projectID) } return nil } // TestZoneInRegion whether zone is in region func TestZoneInRegion(projectID string, zone string, region string) error { regionObject, err := getRegion(projectID, region) if err != nil { return fmt.Errorf(regionError, region, projectID) } zoneObject, err := getZone(projectID, zone) if err != nil { return fmt.Errorf(zoneError, zone, projectID) } if zoneObject.Region != regionObject.SelfLink { return fmt.Errorf(zoneInRegionError, zone, region, projectID) } return nil } func testApisEnabled(bp config.Blueprint, inputs config.Dict) error { if err := checkInputs(inputs, []string{"project_id"}); err != nil { return err } m, err := inputsAsStrings(inputs) if err != nil { return err } apis := map[string]bool{} bp.WalkModulesSafe(func(_ config.ModulePath, m *config.Module) { services := m.InfoOrDie().Metadata.Spec.Requirements.Services for _, api := range services { apis[api] = true } }) return TestApisEnabled(m["project_id"], maps.Keys(apis)) } func testProjectExists(bp config.Blueprint, inputs config.Dict) error { if err := checkInputs(inputs, []string{"project_id"}); err != nil { return err } m, err := inputsAsStrings(inputs) if err != nil { return err } return TestProjectExists(m["project_id"]) } func testRegionExists(bp config.Blueprint, inputs config.Dict) error { if err := checkInputs(inputs, []string{"project_id", "region"}); err != nil { return err } m, err := inputsAsStrings(inputs) if err != nil { return err } return TestRegionExists(m["project_id"], m["region"]) } func testZoneExists(bp config.Blueprint, inputs config.Dict) error { if err := checkInputs(inputs, []string{"project_id", "zone"}); err != nil { return err } m, err := inputsAsStrings(inputs) if err != nil { return err } return TestZoneExists(m["project_id"], m["zone"]) } func testZoneInRegion(bp config.Blueprint, inputs config.Dict) error { if err := checkInputs(inputs, []string{"project_id", "region", "zone"}); err != nil { return err } m, err := inputsAsStrings(inputs) if err != nil { return err } return TestZoneInRegion(m["project_id"], m["zone"], m["region"]) }