golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/buildgo/basepin.go (about)

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package buildgo
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"log"
    12  	"path"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	"golang.org/x/build/dashboard"
    18  	"google.golang.org/api/compute/v1"
    19  )
    20  
    21  // diskPrefix is the name prefix of GCE disks used by MakeBasepinDisks.
    22  const diskPrefix = "basepin-"
    23  
    24  // MakeBasepinDisks looks at the list of all the project's VM images
    25  // and creates (if needed) a disk for each one, named with the prefix
    26  // "basepin-". The purpose of these "basepin" disks is to speed up GCE
    27  // VM creations. When GCE creates a new disk for a new VM, it takes a
    28  // fast path when creating the disk if there's another disk in the
    29  // same zone for that image, and makes the new disk a thin
    30  // Copy-on-Write shadow over the basepin disk. GCE also does this if
    31  // there's been a VM created within the past N minutes of that type,
    32  // but we want VMs to always create quickly.
    33  func (c *Client) MakeBasepinDisks(ctx context.Context) error {
    34  	currentImage := map[string]bool{}
    35  	for _, hc := range dashboard.Hosts {
    36  		if hc.VMImage != "" {
    37  			currentImage[hc.VMImage] = true
    38  		}
    39  	}
    40  
    41  	// Try to find it by name.
    42  	svc := c.Compute()
    43  	imList, err := svc.Images.List(c.Env.ProjectName).Do()
    44  	if err != nil {
    45  		return fmt.Errorf("Error listing images for %s: %v", c.Env.ProjectName, err)
    46  	}
    47  	if imList.NextPageToken != "" {
    48  		return errors.New("too many images; pagination not supported")
    49  	}
    50  
    51  	getNeedMap := func() map[string]*compute.Image {
    52  		need := make(map[string]*compute.Image) // keys like "https://www.googleapis.com/compute/v1/projects/symbolic-datum-552/global/images/linux-buildlet-arm"
    53  		for _, im := range imList.Items {
    54  			imBase := path.Base(im.SelfLink)
    55  			if strings.Contains(im.SelfLink, "-debug") {
    56  				continue
    57  			}
    58  			if !currentImage[imBase] {
    59  				continue
    60  			}
    61  			need[im.SelfLink] = im
    62  		}
    63  		return need
    64  	}
    65  
    66  	for _, zone := range c.Env.VMZones {
    67  		diskList, err := svc.Disks.List(c.Env.ProjectName, zone).Filter(fmt.Sprintf(`name="%s*"`, diskPrefix)).Do()
    68  		if err != nil {
    69  			return fmt.Errorf("error listing disks in zone %v: %v", zone, err)
    70  		}
    71  		if diskList.NextPageToken != "" {
    72  			return fmt.Errorf("too many disks in %v; pagination not supported (yet?)", zone)
    73  		}
    74  
    75  		need := getNeedMap()
    76  
    77  		for _, d := range diskList.Items {
    78  			if si, ok := need[d.SourceImage]; ok && d.SourceImageId == fmt.Sprint(si.Id) {
    79  				if c.Verbose {
    80  					log.Printf("basepin: have %s: %s (%v)\n", d.Name, d.SourceImage, d.SourceImageId)
    81  				}
    82  				delete(need, d.SourceImage)
    83  				continue
    84  			}
    85  			if zone != c.Env.VMZones[0] {
    86  				log.Printf("basepin: deleting unnecessary disk %v in zone %v", d.Name, zone)
    87  				op, err := svc.Disks.Delete(c.Env.ProjectName, zone, d.Name).Do()
    88  				if err != nil {
    89  					log.Printf("basepin: failed to delete disk %v in zone %v: %v", d.Name, zone, err)
    90  					continue
    91  				}
    92  				if err := c.AwaitOp(ctx, op); err != nil {
    93  					log.Printf("basepin: failed to delete disk %v in zone %v: %v", d.Name, zone, err)
    94  					continue
    95  				}
    96  			}
    97  		}
    98  
    99  		var needed []string
   100  		for imageName := range need {
   101  			needed = append(needed, imageName)
   102  		}
   103  		sort.Strings(needed)
   104  		for _, n := range needed {
   105  			log.Printf("basepin: need %v disk in %v", n, zone)
   106  		}
   107  		for i, imName := range needed {
   108  			im := need[imName]
   109  			log.Printf("basepin: (%d/%d) creating %s ...", i+1, len(needed), im.Name)
   110  			op, err := svc.Disks.Insert(c.Env.ProjectName, zone, &compute.Disk{
   111  				Description:   "zone-cached basepin image of " + im.Name,
   112  				Name:          diskPrefix + im.Name + "-" + fmt.Sprint(im.Id),
   113  				SizeGb:        im.DiskSizeGb,
   114  				SourceImage:   im.SelfLink,
   115  				SourceImageId: fmt.Sprint(im.Id),
   116  				Type:          "https://www.googleapis.com/compute/v1/projects/" + c.Env.ProjectName + "/zones/" + zone + "/diskTypes/pd-ssd",
   117  			}).Do()
   118  			if err != nil {
   119  				return err
   120  			}
   121  			if err := c.AwaitOp(ctx, op); err != nil {
   122  				return fmt.Errorf("basepin: failed to create: %v", err)
   123  			}
   124  		}
   125  		log.Printf("basepin: created %d images in %v", len(needed), zone)
   126  	}
   127  	return nil
   128  }
   129  
   130  // AwaitOp waits for op to finish. It returns nil if the operating
   131  // finished successfully.
   132  func (c *Client) AwaitOp(ctx context.Context, op *compute.Operation) error {
   133  	// TODO: clean this up with respect to status updates & logging.
   134  	svc := c.Compute()
   135  	opName := op.Name
   136  	// TODO: move logging to Client c.logger. and add Client.WithLogger shallow copier.
   137  	log.Printf("Waiting on operation %v (in %q)", opName, op.Zone)
   138  	for {
   139  		time.Sleep(2 * time.Second)
   140  		op, err := svc.ZoneOperations.Get(c.Env.ProjectName, path.Base(op.Zone), opName).Do()
   141  		if err != nil {
   142  			return fmt.Errorf("Failed to get op %s: %v", opName, err)
   143  		}
   144  		switch op.Status {
   145  		case "PENDING", "RUNNING":
   146  			log.Printf("Waiting on operation %v", opName)
   147  			continue
   148  		case "DONE":
   149  			if op.Error != nil {
   150  				var last error
   151  				for _, operr := range op.Error.Errors {
   152  					log.Printf("Error: %+v", operr)
   153  					last = fmt.Errorf("%v", operr)
   154  				}
   155  				return last
   156  			}
   157  			return nil
   158  		default:
   159  			return fmt.Errorf("Unknown status %q: %+v", op.Status, op)
   160  		}
   161  	}
   162  }