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 }