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  }