github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/common/utils/param/param_utils.go (about)

     1  //  Copyright 2019 Google Inc. All Rights Reserved.
     2  //
     3  //  Licensed under the Apache License, Version 2.0 (the "License");
     4  //  you may not use this file except in compliance with the License.
     5  //  You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  //  Unless required by applicable law or agreed to in writing, software
    10  //  distributed under the License is distributed on an "AS IS" BASIS,
    11  //  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  //  See the License for the specific language governing permissions and
    13  //  limitations under the License.
    14  
    15  package param
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"regexp"
    21  	"strings"
    22  
    23  	daisy "github.com/GoogleCloudPlatform/compute-daisy"
    24  	daisyCompute "github.com/GoogleCloudPlatform/compute-daisy/compute"
    25  	"google.golang.org/api/option"
    26  
    27  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/domain"
    28  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/paramhelper"
    29  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/storage"
    30  	"github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/common/utils/validation"
    31  )
    32  
    33  // GetProjectID gets project id from flag if exists; otherwise, try to retrieve from GCE metadata.
    34  func GetProjectID(mgce domain.MetadataGCEInterface, projectFlag string) (string, error) {
    35  	if projectFlag == "" {
    36  		if !mgce.OnGCE() {
    37  			return "", daisy.Errf("project cannot be determined because build is not running on GCE")
    38  		}
    39  		aProject, err := mgce.ProjectID()
    40  		if err != nil || aProject == "" {
    41  			return "", daisy.Errf("project cannot be determined %v", err)
    42  		}
    43  		return aProject, nil
    44  	}
    45  	return projectFlag, nil
    46  }
    47  
    48  // populateScratchBucketGcsPath validates the scratch bucket, creating a new one if not
    49  // provided, and returns the region of the scratch bucket. If the scratch bucket is
    50  // already populated, and the owning project doesn't match `project`, then an error is returned.
    51  // In that case, if `file` resides in the non-owned scratch bucket and `removeFileWhenScratchNotOwned`
    52  // is specified, then `file` is deleted from GCS.
    53  func populateScratchBucketGcsPath(scratchBucketGcsPath *string, zone string, mgce domain.MetadataGCEInterface,
    54  	scratchBucketCreator domain.ScratchBucketCreatorInterface, file string, project *string,
    55  	storageClient domain.StorageClientInterface, removeFileWhenScratchNotOwned bool) (string, error) {
    56  
    57  	scratchBucketRegion := ""
    58  	if *scratchBucketGcsPath == "" {
    59  		fallbackZone := zone
    60  		if fallbackZone == "" && mgce.OnGCE() {
    61  			var err error
    62  			if fallbackZone, err = mgce.Zone(); err != nil {
    63  				// reset fallback zone if failed to get zone from running GCE
    64  				fallbackZone = ""
    65  			}
    66  		}
    67  
    68  		scratchBucketName, sbr, err := scratchBucketCreator.CreateScratchBucket(file, *project, fallbackZone)
    69  		scratchBucketRegion = sbr
    70  		if err != nil {
    71  			return "", daisy.Errf("failed to create scratch bucket: %v", err)
    72  		}
    73  
    74  		*scratchBucketGcsPath = fmt.Sprintf("gs://%v/", scratchBucketName)
    75  	} else {
    76  		scratchBucketName, err := storage.GetBucketNameFromGCSPath(*scratchBucketGcsPath)
    77  		if err != nil {
    78  			return "", daisy.Errf("invalid scratch bucket GCS path %v", scratchBucketGcsPath)
    79  		}
    80  
    81  		if !scratchBucketCreator.IsBucketInProject(*project, scratchBucketName) {
    82  			anonymizedErrorMessage := "Scratch bucket %q is not in project %q"
    83  
    84  			substitutions := []interface{}{scratchBucketName, *project}
    85  
    86  			if removeFileWhenScratchNotOwned && strings.HasPrefix(file, fmt.Sprintf("gs://%s/", scratchBucketName)) {
    87  				err := storageClient.DeleteObject(file)
    88  				if err == nil {
    89  					anonymizedErrorMessage += ". Deleted %q"
    90  					substitutions = append(substitutions, file)
    91  				} else {
    92  					anonymizedErrorMessage += ". Failed to delete %q: %v. " +
    93  						"Check with the owner of gs://%q for more information"
    94  					substitutions = append(substitutions, file, err, scratchBucketName)
    95  				}
    96  			}
    97  
    98  			return "", daisy.Errf(anonymizedErrorMessage, substitutions...)
    99  		}
   100  
   101  		scratchBucketAttrs, err := storageClient.GetBucketAttrs(scratchBucketName)
   102  		if err == nil {
   103  			scratchBucketRegion = scratchBucketAttrs.Location
   104  		}
   105  	}
   106  	return scratchBucketRegion, nil
   107  }
   108  
   109  // PopulateProjectIfMissing populates project id for cli tools
   110  func PopulateProjectIfMissing(mgce domain.MetadataGCEInterface, projectFlag *string) error {
   111  	var err error
   112  	*projectFlag, err = GetProjectID(mgce, *projectFlag)
   113  	return err
   114  }
   115  
   116  // PopulateRegion populates region based on the value extracted from zone param
   117  func PopulateRegion(region *string, zone string) error {
   118  	aRegion, err := paramhelper.GetRegion(zone)
   119  	if err != nil {
   120  		return err
   121  	}
   122  	*region = aRegion
   123  	return nil
   124  }
   125  
   126  // CreateComputeClient creates a new compute client
   127  func CreateComputeClient(ctx *context.Context, oauth string, ce string) (daisyCompute.Client, error) {
   128  	computeOptions := []option.ClientOption{option.WithCredentialsFile(oauth)}
   129  	if ce != "" {
   130  		computeOptions = append(computeOptions, option.WithEndpoint(ce))
   131  	}
   132  
   133  	computeClient, err := daisyCompute.NewClient(*ctx, computeOptions...)
   134  	if err != nil {
   135  		return nil, daisy.Errf("failed to create compute client: %v", err)
   136  	}
   137  	return computeClient, nil
   138  }
   139  
   140  var fullResourceURLPrefix = "https://www.googleapis.com/compute/[^/]*/"
   141  var fullResourceURLRegex = regexp.MustCompile(fmt.Sprintf("^(%s)", fullResourceURLPrefix))
   142  
   143  func getResourcePath(scope string, resourceType string, resourceName string) string {
   144  	// handle full URL: transform to relative URL
   145  	if prefix := fullResourceURLRegex.FindString(resourceName); prefix != "" {
   146  		return strings.TrimPrefix(resourceName, prefix)
   147  	}
   148  
   149  	// handle relative (partial) URL: use it as-is
   150  	if strings.Contains(resourceName, "/") {
   151  		return resourceName
   152  	}
   153  
   154  	// handle pure name: treat it as current project
   155  	return fmt.Sprintf("%v/%v/%v", scope, resourceType, resourceName)
   156  }
   157  
   158  // GetImageResourcePath gets the resource path for an image. It will panic if either
   159  // projectID or imageName is invalid. To avoid panic, pre-validate using the
   160  // functions in the `validation` package.
   161  func GetImageResourcePath(projectID, imageName string) string {
   162  	if err := validation.ValidateImageName(imageName); err != nil {
   163  		panic(fmt.Sprintf("Invalid image name %q: %v", imageName, err))
   164  	}
   165  	if err := validation.ValidateProjectID(projectID); err != nil {
   166  		panic(fmt.Sprintf("Invalid projectID %q: %v", projectID, err))
   167  	}
   168  	return fmt.Sprintf("projects/%s/global/images/%s", projectID, imageName)
   169  }
   170  
   171  // GetGlobalResourcePath gets global resource path based on either a local resource name or a path
   172  func GetGlobalResourcePath(resourceType string, resourceName string) string {
   173  	return getResourcePath("global", resourceType, resourceName)
   174  }
   175  
   176  // GetRegionalResourcePath gets regional resource path based on either a local resource name or a path
   177  func GetRegionalResourcePath(region string, resourceType string, resourceName string) string {
   178  	return getResourcePath(fmt.Sprintf("regions/%v", region), resourceType, resourceName)
   179  }
   180  
   181  // GetZonalResourcePath gets zonal resource path based on either a local resource name or a path
   182  func GetZonalResourcePath(zone string, resourceType string, resourceName string) string {
   183  	return getResourcePath(fmt.Sprintf("zones/%v", zone), resourceType, resourceName)
   184  }