github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/gce_image_publish/main.go (about) 1 // Copyright 2017 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 // gce_image_publish is a tool for publishing GCE images. 16 package main 17 18 import ( 19 "context" 20 "flag" 21 "fmt" 22 "os" 23 "os/signal" 24 "regexp" 25 "strings" 26 "sync" 27 "time" 28 29 daisy "github.com/GoogleCloudPlatform/compute-daisy" 30 computeAlpha "google.golang.org/api/compute/v0.alpha" 31 32 "github.com/GoogleCloudPlatform/compute-image-tools/cli_tools/gce_image_publish/publish" 33 ) 34 35 var ( 36 oauth = flag.String("oauth", "", "path to oauth json file") 37 workProject = flag.String("work_project", "", "project to perform the work in, passed to Daisy as workflow project, will override WorkProject in template") 38 sourceVersion = flag.String("source_version", "v"+time.Now().UTC().Format("20060102"), "version on source image") 39 sourceGCS = flag.String("source_gcs_path", "", "GCS path to source images from, should not be used with source_project, will override SourceGCSPath in template") 40 sourceProject = flag.String("source_project", "", "project to source images from, should not be used with source_gcs_path, will override SourceProject in template") 41 publishVersion = flag.String("publish_version", "", "version for published image if different from source") 42 publishProject = flag.String("publish_project", "", "project to publish images to, will override PublishProject in template") 43 skipDup = flag.Bool("skip_duplicates", false, "skip publishing any images that already exist, should not be used along with -replace") 44 noRoot = flag.Bool("no_root", false, "with -source_gcs_path, append .tar.gz instead of /root.tar.gz") 45 replace = flag.Bool("replace", false, "replace any images that already exist, should not be used along with -skip_duplicates") 46 rollback = flag.Bool("rollback", false, "rollback image publish") 47 print = flag.Bool("print", false, "print out the parsed workflow for debugging") 48 validate = flag.Bool("validate", false, "validate the workflow and exit") 49 noConfirm = flag.Bool("skip_confirmation", false, "don't ask for confirmation") 50 ce = flag.String("compute_endpoint_override", "", "API endpoint to override default, will override ComputeEndpoint in template") 51 filter = flag.String("filter", "", "regular expression to filter images to publish by prefixes") 52 rolloutRate = flag.Int("rollout_rate", 60, "The number of minutes between the image rolling out between zones. 0 minutes will not use a rollout policy.") 53 ) 54 55 const ( 56 flgDefValue = "flag generated for workflow variable" 57 varFlagPrefix = "var:" 58 ) 59 60 func addFlags(args []string) { 61 for _, arg := range args { 62 if len(arg) <= 1 || arg[0] != '-' { 63 continue 64 } 65 66 name := arg[1:] 67 if name[0] == '-' { 68 name = name[1:] 69 } 70 71 if !strings.HasPrefix(name, varFlagPrefix) { 72 continue 73 } 74 75 name = strings.SplitN(name, "=", 2)[0] 76 77 if flag.Lookup(name) != nil { 78 continue 79 } 80 81 flag.String(name, "", flgDefValue) 82 } 83 } 84 85 func checkError(errors chan error) { 86 select { 87 case err := <-errors: 88 fmt.Fprintln(os.Stderr, "\n[Publish] Errors in one or more workflows:") 89 fmt.Fprintln(os.Stderr, " ", err) 90 for { 91 select { 92 case err := <-errors: 93 fmt.Fprintln(os.Stderr, " ", err) 94 continue 95 default: 96 os.Exit(1) 97 } 98 } 99 default: 100 return 101 } 102 } 103 104 func main() { 105 addFlags(os.Args[1:]) 106 flag.Parse() 107 108 varMap := map[string]string{} 109 flag.Visit(func(flg *flag.Flag) { 110 if strings.HasPrefix(flg.Name, varFlagPrefix) { 111 varMap[strings.TrimPrefix(flg.Name, varFlagPrefix)] = flg.Value.String() 112 } 113 }) 114 115 if *rolloutRate < 0 { 116 fmt.Println("-rollout_rate cannot be less than 0.") 117 os.Exit(1) 118 } 119 120 if *skipDup && *replace { 121 fmt.Println("Cannot set both -skip_duplicates and -replace") 122 os.Exit(1) 123 } 124 125 if len(flag.Args()) == 0 { 126 fmt.Println("Not enough args, first arg needs to be the path to a publish template.") 127 os.Exit(1) 128 } 129 var regex *regexp.Regexp 130 if *filter != "" { 131 var err error 132 regex, err = regexp.Compile(*filter) 133 if err != nil { 134 fmt.Println("-filter flag not valid:", err) 135 os.Exit(1) 136 } 137 } 138 139 ctx := context.Background() 140 141 var errs []error 142 var ws []*daisy.Workflow 143 imagesCache := map[string][]*computeAlpha.Image{} 144 for _, path := range flag.Args() { 145 p, err := publish.CreatePublish( 146 *sourceVersion, *publishVersion, *workProject, *publishProject, *sourceGCS, *sourceProject, *ce, path, varMap, imagesCache) 147 if err != nil { 148 loadErr := fmt.Errorf("Loading publish error %s from %q", err, path) 149 fmt.Println(loadErr) 150 errs = append(errs, loadErr) 151 continue 152 } 153 w, err := p.CreateWorkflows(ctx, varMap, regex, *rollback, *skipDup, *replace, *noRoot, *oauth, time.Now(), *rolloutRate) 154 if err != nil { 155 createWorkflowErr := fmt.Errorf("Workflow creation error: %s", err) 156 fmt.Println(createWorkflowErr) 157 errs = append(errs, createWorkflowErr) 158 continue 159 } 160 if w != nil { 161 ws = append(ws, w...) 162 } 163 } 164 165 errors := make(chan error, len(ws)+len(errs)) 166 for _, err := range errs { 167 errors <- err 168 } 169 if len(ws) == 0 { 170 checkError(errors) 171 fmt.Println("[Publish] Nothing to do") 172 return 173 } 174 175 if *print { 176 for _, w := range ws { 177 fmt.Printf("[Publish] Printing workflow %q\n", w.Name) 178 w.Print(ctx) 179 } 180 checkError(errors) 181 return 182 } 183 184 if *validate { 185 for _, w := range ws { 186 fmt.Printf("[Publish] Validating workflow %q\n", w.Name) 187 if err := w.Validate(ctx); err != nil { 188 errors <- fmt.Errorf("Error validating workflow %s: %v", w.Name, err) 189 } 190 } 191 checkError(errors) 192 return 193 } 194 195 if !*noConfirm { 196 var c string 197 fmt.Print("\nContinue with publish? (y/N): ") 198 fmt.Scanln(&c) 199 c = strings.ToLower(c) 200 if c != "y" && c != "yes" { 201 return 202 } 203 } 204 205 var wg sync.WaitGroup 206 for _, w := range ws { 207 c := make(chan os.Signal, 1) 208 signal.Notify(c, os.Interrupt) 209 go func(w *daisy.Workflow) { 210 select { 211 case <-c: 212 fmt.Printf("\nCtrl-C caught, sending cancel signal to %q...\n", w.Name) 213 w.CancelWorkflow() 214 errors <- fmt.Errorf("workflow %q was canceled", w.Name) 215 case <-w.Cancel: 216 } 217 }(w) 218 219 wg.Add(1) 220 go func(w *daisy.Workflow) { 221 defer wg.Done() 222 fmt.Printf("[Publish] Running workflow %q\n", w.Name) 223 if err := w.Run(ctx); err != nil { 224 errors <- fmt.Errorf("%s: %v", w.Name, err) 225 return 226 } 227 fmt.Printf("[Publish] Workflow %q finished\n", w.Name) 228 }(w) 229 } 230 wg.Wait() 231 232 checkError(errors) 233 fmt.Println("[Publish] Workflows completed successfully.") 234 }