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 }