github.com/GoogleCloudPlatform/compute-image-tools/cli_tools@v0.0.0-20240516224744-de2dabc4ed1b/gce_image_publish/publish/publish.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 publish defines the publish object and utilities to create daisy workflows
    16  // from a publish object.
    17  package publish
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"path"
    27  	"regexp"
    28  	"sort"
    29  	"strconv"
    30  	"strings"
    31  	"text/template"
    32  	"time"
    33  
    34  	"cloud.google.com/go/compute/metadata"
    35  	daisy "github.com/GoogleCloudPlatform/compute-daisy"
    36  	daisyCompute "github.com/GoogleCloudPlatform/compute-daisy/compute"
    37  	computeAlpha "google.golang.org/api/compute/v0.alpha"
    38  	"google.golang.org/api/compute/v1"
    39  	"google.golang.org/api/option"
    40  )
    41  
    42  // Publish holds info to create a daisy workflow for gce_image_publish
    43  type Publish struct {
    44  	// Name for this publish workflow, passed to Daisy as workflow name.
    45  	Name string `json:",omitempty"`
    46  	// Project to perform the work in, passed to Daisy as workflow project.
    47  	WorkProject string `json:",omitempty"`
    48  	// Project to source images from, should not be used with SourceGCSPath.
    49  	SourceProject string `json:",omitempty"`
    50  	// GCS path to source images from, should not be used with SourceProject.
    51  	SourceGCSPath string `json:",omitempty"`
    52  	// Project to publish images to.
    53  	PublishProject string `json:",omitempty"`
    54  	// Optional compute endpoint override
    55  	ComputeEndpoint string `json:",omitempty"`
    56  	// Optional period of time to keep images, any images with an create time
    57  	// older than this period will be deleted.
    58  	// Format consists of 2 sections, the first must parsable by
    59  	// https://golang.org/pkg/time/#ParseDuration, the second is a multiplier
    60  	// separated by '*'.
    61  	// 24h = 1 day
    62  	// 24h*7 = 1 week
    63  	// 24h*7*4 = ~1 month
    64  	// 24h*365 = ~1 year
    65  	DeleteAfter string `json:",omitempty"`
    66  	expiryDate  *time.Time
    67  	// Images to
    68  	Images []*Image `json:",omitempty"`
    69  
    70  	// Populated from the source_version flag, added to the image prefix to
    71  	// lookup source image.
    72  	sourceVersion string
    73  	// Populated from the publish_version flag, added to the image prefix to
    74  	// create the publish name.
    75  	publishVersion string
    76  
    77  	toCreate      []string
    78  	toDelete      []string
    79  	toDeprecate   []string
    80  	toObsolete    []string
    81  	toUndeprecate []string
    82  
    83  	rolloutPolicy []string
    84  
    85  	imagesCache map[string][]*computeAlpha.Image
    86  }
    87  
    88  // Image is a metadata holder for the image to be published/rollback
    89  type Image struct {
    90  	// Prefix for the image, image naming format is '${ImagePrefix}-${ImageVersion}'.
    91  	// This prefix is used for source image lookup and publish image name.
    92  	Prefix string `json:",omitempty"`
    93  	// Image family to set for the image.
    94  	Family string `json:",omitempty"`
    95  	// Image description to set for the image.
    96  	Description string `json:",omitempty"`
    97  	// Architecture to set for the image.
    98  	Architecture string `json:",omitempty"`
    99  	// Licenses to add to the image.
   100  	Licenses []string `json:",omitempty"`
   101  	// GuestOsFeatures to add to the image.
   102  	GuestOsFeatures []string `json:",omitempty"`
   103  	// Ignores license validation if 403/forbidden returned
   104  	IgnoreLicenseValidationIfForbidden bool `json:",omitempty"`
   105  	// Optional DeprecationStatus.Obsolete entry for the image (RFC 3339).
   106  	ObsoleteDate *time.Time `json:",omitempty"`
   107  	// Optional ShieldedInstanceInitialState entry for secure-boot feature.
   108  	ShieldedInstanceInitialState *computeAlpha.InitialStateConfig `json:",omitempty"`
   109  	// RolloutPolicy entry for the image rollout policy.
   110  	RolloutPolicy *computeAlpha.RolloutPolicy `json:",omitempty"`
   111  }
   112  
   113  var (
   114  	funcMap = template.FuncMap{
   115  		"trim":       strings.Trim,
   116  		"trimPrefix": strings.TrimPrefix,
   117  		"trimSuffix": strings.TrimSuffix,
   118  	}
   119  	publishTemplate = template.New("publishTemplate").Option("missingkey=zero").Funcs(funcMap)
   120  )
   121  
   122  // CreatePublish creates a publish object
   123  func CreatePublish(sourceVersion, publishVersion, workProject, publishProject, sourceGCS, sourceProject, ce, path string, varMap map[string]string, imagesCache map[string][]*computeAlpha.Image) (*Publish, error) {
   124  	b, err := ioutil.ReadFile(path)
   125  	if err != nil {
   126  		return nil, fmt.Errorf("%s: %v", path, err)
   127  	}
   128  	templateContent := string(b)
   129  	return createPublish(sourceVersion, publishVersion, workProject, publishProject, sourceGCS, sourceProject, ce, templateContent, varMap, imagesCache)
   130  }
   131  
   132  // CreatePublishWithTemplate creates a publish object without reading a template file
   133  func CreatePublishWithTemplate(sourceVersion, publishVersion, workProject, publishProject, sourceGCS, sourceProject, ce, template string, varMap map[string]string, imagesCache map[string][]*computeAlpha.Image) (*Publish, error) {
   134  	return createPublish(sourceVersion, publishVersion, workProject, publishProject, sourceGCS, sourceProject, ce, template, varMap, imagesCache)
   135  }
   136  
   137  func createPublish(sourceVersion, publishVersion, workProject, publishProject, sourceGCS, sourceProject, ce, template string, varMap map[string]string, imagesCache map[string][]*computeAlpha.Image) (*Publish, error) {
   138  	p := Publish{
   139  		sourceVersion:  sourceVersion,
   140  		publishVersion: publishVersion,
   141  	}
   142  	if p.publishVersion == "" {
   143  		p.publishVersion = sourceVersion
   144  	}
   145  	varMap["source_version"] = p.sourceVersion
   146  	varMap["publish_version"] = p.publishVersion
   147  
   148  	tmpl, err := publishTemplate.Parse(template)
   149  	if err != nil {
   150  		return nil, fmt.Errorf("%s: %v", template, err)
   151  	}
   152  
   153  	var buf bytes.Buffer
   154  	if err := tmpl.Execute(&buf, varMap); err != nil {
   155  		return nil, fmt.Errorf("%s: %v", template, err)
   156  	}
   157  
   158  	if err := json.Unmarshal(buf.Bytes(), &p); err != nil {
   159  		return nil, daisy.JSONError(template, buf.Bytes(), err)
   160  	}
   161  
   162  	if err := p.SetExpire(); err != nil {
   163  		return nil, fmt.Errorf("%s: error SetExpire: %v", template, err)
   164  	}
   165  
   166  	if workProject != "" {
   167  		p.WorkProject = workProject
   168  	}
   169  	if publishProject != "" {
   170  		p.PublishProject = publishProject
   171  	}
   172  	if sourceGCS != "" {
   173  		p.SourceGCSPath = sourceGCS
   174  	}
   175  	if sourceProject != "" {
   176  		p.SourceProject = sourceProject
   177  	}
   178  	if ce != "" {
   179  		p.ComputeEndpoint = ce
   180  	}
   181  	if imagesCache != nil {
   182  		p.imagesCache = imagesCache
   183  	}
   184  	if p.WorkProject == "" {
   185  		if metadata.OnGCE() {
   186  			p.WorkProject, err = metadata.ProjectID()
   187  			if err != nil {
   188  				return nil, err
   189  			}
   190  		} else {
   191  			return nil, fmt.Errorf("%s\nWorkProject unspecified", template)
   192  		}
   193  	}
   194  
   195  	fmt.Printf("[%q] Created a publish object successfully from %s\n", p.Name, template)
   196  	return &p, nil
   197  }
   198  
   199  // SetExpire converts p.DeleteAfter into p.expiryDate
   200  func (p *Publish) SetExpire() error {
   201  	expire, err := calculateExpiryDate(p.DeleteAfter)
   202  	if err != nil {
   203  		return fmt.Errorf("error parsing DeleteAfter: %v", err)
   204  	}
   205  	p.expiryDate = expire
   206  	return nil
   207  }
   208  
   209  // CreateWorkflows creates a list of daisy workflows from the publish object
   210  func (p *Publish) CreateWorkflows(ctx context.Context, varMap map[string]string, regex *regexp.Regexp, rollback, skipDup, replace, noRoot bool, oauth string, rolloutStartTime time.Time, rolloutRate int, clientOptions ...option.ClientOption) ([]*daisy.Workflow, error) {
   211  	fmt.Printf("[%q] Preparing workflows from template\n", p.Name)
   212  
   213  	var ws []*daisy.Workflow
   214  	for _, img := range p.Images {
   215  		if regex != nil && !regex.MatchString(img.Prefix) {
   216  			continue
   217  		}
   218  		w, err := p.createWorkflow(ctx, img, varMap, rollback, skipDup, replace, noRoot, oauth, rolloutStartTime, rolloutRate, clientOptions...)
   219  		if err != nil {
   220  			return nil, err
   221  		}
   222  		if w == nil {
   223  			continue
   224  		}
   225  		ws = append(ws, w)
   226  	}
   227  	if len(ws) == 0 {
   228  		fmt.Println("  Nothing to do.")
   229  		return nil, nil
   230  	}
   231  
   232  	if len(p.toCreate) > 0 {
   233  		fmt.Printf("  The following images will be created in %q:\n", p.PublishProject)
   234  		printList(p.toCreate)
   235  	}
   236  
   237  	if len(p.toDeprecate) > 0 {
   238  		fmt.Printf("  The following images will be deprecated in %q:\n", p.PublishProject)
   239  		printList(p.toDeprecate)
   240  	}
   241  
   242  	if len(p.toObsolete) > 0 {
   243  		fmt.Printf("  The following images will be obsoleted in %q:\n", p.PublishProject)
   244  		printList(p.toObsolete)
   245  	}
   246  
   247  	if len(p.toUndeprecate) > 0 {
   248  		fmt.Printf("  The following images will be un-deprecated in %q:\n", p.PublishProject)
   249  		printList(p.toUndeprecate)
   250  	}
   251  
   252  	if len(p.toDelete) > 0 {
   253  		fmt.Printf("  The following images will be deleted in %q:\n", p.PublishProject)
   254  		printList(p.toDelete)
   255  	}
   256  
   257  	if len(p.rolloutPolicy) > 0 {
   258  		fmt.Println("  All images will have the following rollout policy:")
   259  		printList(p.rolloutPolicy)
   260  	}
   261  
   262  	return ws, nil
   263  }
   264  
   265  // ------------------ private methods -------------------------
   266  
   267  const gcsImageObj = "root.tar.gz"
   268  
   269  func publishImage(p *Publish, img *Image, pubImgs []*computeAlpha.Image, skipDuplicates, rep, noRoot bool) (*daisy.CreateImages, *daisy.DeprecateImages, *daisy.DeleteResources, error) {
   270  	if skipDuplicates && rep {
   271  		return nil, nil, nil, errors.New("cannot set both skipDuplicates and replace")
   272  	}
   273  
   274  	publishName := img.Prefix
   275  	if p.publishVersion != "" {
   276  		publishName = fmt.Sprintf("%s-%s", publishName, p.publishVersion)
   277  	}
   278  	sourceName := img.Prefix
   279  	if p.sourceVersion != "" {
   280  		sourceName = fmt.Sprintf("%s-%s", sourceName, p.sourceVersion)
   281  	}
   282  
   283  	var ds *computeAlpha.DeprecationStatus
   284  	if img.ObsoleteDate != nil {
   285  		ds = &computeAlpha.DeprecationStatus{
   286  			State:    "ACTIVE",
   287  			Obsolete: img.ObsoleteDate.Format(time.RFC3339),
   288  		}
   289  	}
   290  
   291  	ci := daisy.ImageAlpha{
   292  		Image: computeAlpha.Image{
   293  			Name:                         publishName,
   294  			Description:                  img.Description,
   295  			Architecture:                 img.Architecture,
   296  			Licenses:                     img.Licenses,
   297  			Family:                       img.Family,
   298  			Deprecated:                   ds,
   299  			ShieldedInstanceInitialState: img.ShieldedInstanceInitialState,
   300  			RolloutOverride:              img.RolloutPolicy,
   301  		},
   302  		ImageBase: daisy.ImageBase{
   303  			Resource: daisy.Resource{
   304  				NoCleanup: true,
   305  				Project:   p.PublishProject,
   306  				RealName:  publishName,
   307  			},
   308  			IgnoreLicenseValidationIfForbidden: img.IgnoreLicenseValidationIfForbidden,
   309  		},
   310  		GuestOsFeatures: img.GuestOsFeatures,
   311  	}
   312  
   313  	var source string
   314  	if p.SourceProject != "" && p.SourceGCSPath != "" {
   315  		return nil, nil, nil, errors.New("only one of SourceProject or SourceGCSPath should be set")
   316  	}
   317  	if p.SourceProject != "" {
   318  		source = fmt.Sprintf("projects/%s/global/images/%s", p.SourceProject, sourceName)
   319  		ci.Image.SourceImage = source
   320  	} else if p.SourceGCSPath != "" {
   321  		if noRoot {
   322  			source = fmt.Sprintf("%s/%s.tar.gz", p.SourceGCSPath, sourceName)
   323  		} else {
   324  			source = fmt.Sprintf("%s/%s/%s", p.SourceGCSPath, sourceName, gcsImageObj)
   325  		}
   326  		ci.Image.RawDisk = &computeAlpha.ImageRawDisk{Source: source}
   327  	} else {
   328  		return nil, nil, nil, errors.New("neither SourceProject or SourceGCSPath was set")
   329  	}
   330  	cis := &daisy.CreateImages{ImagesAlpha: []*daisy.ImageAlpha{&ci}}
   331  
   332  	dis := &daisy.DeprecateImages{}
   333  	drs := &daisy.DeleteResources{}
   334  	for _, pubImg := range pubImgs {
   335  		if pubImg.Name == publishName {
   336  			msg := fmt.Sprintf("%q already exists in project %q", publishName, p.PublishProject)
   337  			if skipDuplicates {
   338  				fmt.Printf("    Image %s, skipping image creation\n", msg)
   339  				cis = nil
   340  				continue
   341  			} else if rep {
   342  				fmt.Printf("    Image %s, replacing\n", msg)
   343  				(*cis).ImagesAlpha[0].OverWrite = true
   344  				continue
   345  			}
   346  			return nil, nil, nil, errors.New(msg)
   347  		}
   348  
   349  		if pubImg.Family != img.Family {
   350  			continue
   351  		}
   352  
   353  		// Delete all images in the same family with insert date older than p.expiryDate.
   354  		if p.expiryDate != nil {
   355  			createTime, err := time.Parse(time.RFC3339, pubImg.CreationTimestamp)
   356  			if err != nil {
   357  				continue
   358  			}
   359  			if createTime.Before(*p.expiryDate) {
   360  				drs.Images = append(drs.Images, fmt.Sprintf("projects/%s/global/images/%s", p.PublishProject, pubImg.Name))
   361  				continue
   362  			}
   363  		}
   364  
   365  		if pubImg.Family == "" {
   366  			continue
   367  		}
   368  
   369  		// Deprecate all images in the same family.
   370  		if pubImg.Deprecated == nil || pubImg.Deprecated.State == "" {
   371  			*dis = append(*dis, &daisy.DeprecateImage{
   372  				Image:   pubImg.Name,
   373  				Project: p.PublishProject,
   374  				DeprecationStatusAlpha: computeAlpha.DeprecationStatus{
   375  					State:         "DEPRECATED",
   376  					Replacement:   fmt.Sprintf(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/%s", p.PublishProject, publishName)),
   377  					StateOverride: img.RolloutPolicy,
   378  				},
   379  			})
   380  		}
   381  	}
   382  	if len(*dis) == 0 {
   383  		dis = nil
   384  	}
   385  	if len(drs.Images) == 0 {
   386  		drs = nil
   387  	}
   388  
   389  	return cis, dis, drs, nil
   390  }
   391  
   392  func rollbackImage(p *Publish, img *Image, pubImgs []*computeAlpha.Image) (*daisy.DeleteResources, *daisy.DeprecateImages) {
   393  	publishName := fmt.Sprintf("%s-%s", img.Prefix, p.publishVersion)
   394  	dr := &daisy.DeleteResources{}
   395  	dis := &daisy.DeprecateImages{}
   396  	for _, pubImg := range pubImgs {
   397  		if pubImg.Name != publishName || pubImg.Deprecated != nil {
   398  			continue
   399  		}
   400  		dr.Images = []string{fmt.Sprintf("projects/%s/global/images/%s", p.PublishProject, publishName)}
   401  	}
   402  
   403  	if len(dr.Images) == 0 {
   404  		fmt.Printf("   %q does not exist in %q, not rolling back\n", publishName, p.PublishProject)
   405  		return nil, nil
   406  	}
   407  
   408  	for _, pubImg := range pubImgs {
   409  		// Un-deprecate the first deprecated image in the family based on insertion time.
   410  		if pubImg.Family == img.Family && pubImg.Deprecated != nil {
   411  			*dis = append(*dis, &daisy.DeprecateImage{
   412  				Image:   pubImg.Name,
   413  				Project: p.PublishProject,
   414  				DeprecationStatusAlpha: computeAlpha.DeprecationStatus{
   415  					State:         "ACTIVE",
   416  					StateOverride: img.RolloutPolicy,
   417  				},
   418  			})
   419  			break
   420  		}
   421  	}
   422  	return dr, dis
   423  }
   424  
   425  func populateSteps(w *daisy.Workflow, prefix string, createImages *daisy.CreateImages, deprecateImages *daisy.DeprecateImages, deleteResources *daisy.DeleteResources) error {
   426  	var createStep *daisy.Step
   427  	var deprecateStep *daisy.Step
   428  	var deleteStep *daisy.Step
   429  	var err error
   430  	if createImages != nil {
   431  		createStep, err = w.NewStep("publish-" + prefix)
   432  		if err != nil {
   433  			return err
   434  		}
   435  		createStep.CreateImages = createImages
   436  		// The default of 10m is a bit low, 1h is excessive for most use cases.
   437  		// TODO(ajackura): Maybe add a timeout field override to the template?
   438  		createStep.Timeout = "1h"
   439  	}
   440  
   441  	if deprecateImages != nil {
   442  		deprecateStep, err = w.NewStep("deprecate-" + prefix)
   443  		if err != nil {
   444  			return err
   445  		}
   446  		deprecateStep.DeprecateImages = deprecateImages
   447  	}
   448  
   449  	if deleteResources != nil {
   450  		deleteStep, err = w.NewStep("delete-" + prefix)
   451  		if err != nil {
   452  			return err
   453  		}
   454  		deleteStep.DeleteResources = deleteResources
   455  	}
   456  
   457  	// Create before deprecate on
   458  	if deprecateStep != nil && createStep != nil {
   459  		w.AddDependency(deprecateStep, createStep)
   460  	}
   461  
   462  	// Create before delete on
   463  	if deleteStep != nil && createStep != nil {
   464  		w.AddDependency(deleteStep, createStep)
   465  	}
   466  
   467  	// Un-deprecate before delete on rollback.
   468  	if deleteStep != nil && deprecateStep != nil {
   469  		w.AddDependency(deleteStep, deprecateStep)
   470  	}
   471  
   472  	return nil
   473  }
   474  
   475  func (p *Publish) createPrintOut(createImages *daisy.CreateImages) {
   476  	if createImages == nil {
   477  		return
   478  	}
   479  	for _, ci := range createImages.ImagesAlpha {
   480  		p.toCreate = append(p.toCreate, fmt.Sprintf("%s: (%s)", ci.Name, ci.Description))
   481  	}
   482  	return
   483  }
   484  
   485  func (p *Publish) deletePrintOut(deleteResources *daisy.DeleteResources) {
   486  	if deleteResources == nil {
   487  		return
   488  	}
   489  
   490  	for _, img := range deleteResources.Images {
   491  		p.toDelete = append(p.toDelete, path.Base(img))
   492  	}
   493  }
   494  
   495  func (p *Publish) deprecatePrintOut(deprecateImages *daisy.DeprecateImages) {
   496  	if deprecateImages == nil {
   497  		return
   498  	}
   499  
   500  	for _, di := range *deprecateImages {
   501  		image := path.Base(di.Image)
   502  		switch di.DeprecationStatusAlpha.State {
   503  		case "DEPRECATED":
   504  			p.toDeprecate = append(p.toDeprecate, image)
   505  		case "OBSOLETE":
   506  			p.toObsolete = append(p.toObsolete, image)
   507  		case "ACTIVE", "":
   508  			p.toUndeprecate = append(p.toUndeprecate, image)
   509  		}
   510  	}
   511  }
   512  
   513  func (p *Publish) rolloutPolicyPrintOut(rp *computeAlpha.RolloutPolicy) {
   514  	p.rolloutPolicy = append(p.rolloutPolicy, fmt.Sprintf("Default rollout time: %s", rp.DefaultRolloutTime))
   515  	var zones []string
   516  	for k := range rp.LocationRolloutPolicies {
   517  		zones = append(zones, k)
   518  	}
   519  	sort.Strings(zones)
   520  
   521  	for _, v := range zones {
   522  		p.rolloutPolicy = append(p.rolloutPolicy, fmt.Sprintf("Zone %s at %s", v[6:], rp.LocationRolloutPolicies[v]))
   523  	}
   524  }
   525  
   526  func (p *Publish) populateWorkflow(ctx context.Context, w *daisy.Workflow, pubImgs []*computeAlpha.Image, img *Image, rb, sd, rep, noRoot bool) error {
   527  	var err error
   528  	var createImages *daisy.CreateImages
   529  	var deprecateImages *daisy.DeprecateImages
   530  	var deleteResources *daisy.DeleteResources
   531  
   532  	if rb {
   533  		deleteResources, deprecateImages = rollbackImage(p, img, pubImgs)
   534  	} else {
   535  		createImages, deprecateImages, deleteResources, err = publishImage(p, img, pubImgs, sd, rep, noRoot)
   536  		if err != nil {
   537  			return err
   538  		}
   539  	}
   540  
   541  	if err := populateSteps(w, img.Prefix, createImages, deprecateImages, deleteResources); err != nil {
   542  		return err
   543  	}
   544  
   545  	p.createPrintOut(createImages)
   546  	p.deletePrintOut(deleteResources)
   547  	p.deprecatePrintOut(deprecateImages)
   548  	p.rolloutPolicyPrintOut(img.RolloutPolicy)
   549  
   550  	return nil
   551  }
   552  
   553  func (p *Publish) createWorkflow(ctx context.Context, img *Image, varMap map[string]string, rb, sd, rep, noRoot bool, oauth string, rolloutStartTime time.Time, rolloutRate int, clientOptions ...option.ClientOption) (*daisy.Workflow, error) {
   554  	fmt.Printf("  - Creating publish workflow for %q\n", img.Prefix)
   555  	w := daisy.New()
   556  	for k, v := range varMap {
   557  		w.AddVar(k, v)
   558  	}
   559  
   560  	if oauth != "" {
   561  		w.OAuthPath = oauth
   562  	}
   563  
   564  	if p.ComputeEndpoint != "" {
   565  		w.ComputeEndpoint = p.ComputeEndpoint
   566  	}
   567  
   568  	if err := w.PopulateClients(ctx, clientOptions...); err != nil {
   569  		return nil, fmt.Errorf("PopulateClients failed: %s", err)
   570  	}
   571  
   572  	w.Name = img.Prefix
   573  	w.Project = p.WorkProject
   574  
   575  	cacheKey := w.ComputeClient.BasePath() + p.PublishProject
   576  
   577  	pubImgs, ok := p.imagesCache[cacheKey]
   578  	if !ok {
   579  		var err error
   580  		pubImgs, err = w.ComputeClient.ListImagesAlpha(p.PublishProject, daisyCompute.OrderBy("creationTimestamp desc"))
   581  		if err != nil {
   582  			return nil, fmt.Errorf("computeClient.ListImagesAlpha failed: %s", err)
   583  		}
   584  		if p.imagesCache != nil {
   585  			p.imagesCache[cacheKey] = pubImgs
   586  		}
   587  	}
   588  
   589  	zones, err := w.ComputeClient.ListZones(w.Project)
   590  	if err != nil {
   591  		return nil, fmt.Errorf("computeClient.GetZone failed: %s", err)
   592  	}
   593  	img.RolloutPolicy = createRollOut(zones, rolloutStartTime, rolloutRate)
   594  
   595  	if err := p.populateWorkflow(ctx, w, pubImgs, img, rb, sd, rep, noRoot); err != nil {
   596  		return nil, fmt.Errorf("populateWorkflow failed: %s", err)
   597  	}
   598  	if len(w.Steps) == 0 {
   599  		return nil, nil
   600  	}
   601  	return w, nil
   602  }
   603  
   604  func printList(list []string) {
   605  	for _, i := range list {
   606  		fmt.Printf("   - [ %s ]\n", i)
   607  	}
   608  }
   609  
   610  func calculateExpiryDate(deleteAfter string) (*time.Time, error) {
   611  	if deleteAfter == "" {
   612  		return nil, nil
   613  	}
   614  	split := strings.Split(deleteAfter, "*")
   615  	base, err := time.ParseDuration(split[0])
   616  	if err != nil {
   617  		return nil, err
   618  	}
   619  	m := 1
   620  	for i, s := range split {
   621  		if i == 0 {
   622  			continue
   623  		}
   624  		nm, err := strconv.Atoi(s)
   625  		if err != nil {
   626  			return nil, err
   627  		}
   628  		m = m * nm
   629  	}
   630  	deleteTime := base * time.Duration(m)
   631  	expiryDate := time.Now().UTC().Add(-deleteTime)
   632  
   633  	return &expiryDate, nil
   634  }
   635  
   636  func createRollOut(zones []*compute.Zone, rolloutStartTime time.Time, rolloutRate int) *computeAlpha.RolloutPolicy {
   637  	rp := computeAlpha.RolloutPolicy{}
   638  
   639  	var regions map[string][]string
   640  	regions = make(map[string][]string)
   641  	maxRegionLength := 0
   642  
   643  	// Build a map of all the regions and determine the max number of zones in a region.
   644  	for _, z := range zones {
   645  		regions[z.Region] = append(regions[z.Region], z.Name)
   646  		if len(regions[z.Region]) > maxRegionLength {
   647  			maxRegionLength = len(regions[z.Region])
   648  		}
   649  	}
   650  
   651  	// Order the list of zones in each region.
   652  	for _, value := range regions {
   653  		sort.Strings(value)
   654  	}
   655  
   656  	// zoneList is the ordered list of zones to apply the rollout policy to.
   657  	var zoneList []string
   658  
   659  	// zoneList's order should be the first zone from each region, then second zone from each region, third zone from each region, etc.
   660  	// us-central1-a, us-central2-b, us-central3-c, us-central1-a, us-central2-b, us-central3-c
   661  	for zoneCount := 0; zoneCount < maxRegionLength; zoneCount++ {
   662  		for _, regionZones := range regions {
   663  			// If the region has a zone at the current zoneCount, add that zone to the zoneList.
   664  			if zoneCount < len(regionZones) {
   665  				zoneList = append(zoneList, regionZones[zoneCount])
   666  			}
   667  		}
   668  	}
   669  
   670  	var rolloutPolicy map[string]string
   671  	rolloutPolicy = make(map[string]string)
   672  
   673  	for i, zone := range zoneList {
   674  		rolloutPolicy[fmt.Sprintf("zones/%s", zone)] = rolloutStartTime.Add(time.Duration(rolloutRate*i) * time.Minute).Format(time.RFC3339)
   675  	}
   676  
   677  	// Set the default time to be the same as the last zone.
   678  	rp.DefaultRolloutTime = rolloutStartTime.Add(time.Duration(rolloutRate*(len(zoneList)-1)) * time.Minute).Format(time.RFC3339)
   679  	rp.LocationRolloutPolicies = rolloutPolicy
   680  	return &rp
   681  }