golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/xb/xb.go (about)

     1  // Copyright 2018 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  // The xb command wraps GCP deployment commands such as gcloud,
     6  // kubectl, and docker push and verifies they're interacting with the
     7  // intended prod-vs-staging environment.
     8  //
     9  // Usage:
    10  //
    11  //	xb {--prod,--staging} <CMD> [<ARGS>...]
    12  //
    13  // Examples:
    14  //
    15  //	xb --staging kubectl ...
    16  //	xb --prod kubectl ...
    17  //	xb google-email  # print the @google.com account from gcloud
    18  package main // import "golang.org/x/build/cmd/xb"
    19  
    20  import (
    21  	"bufio"
    22  	"flag"
    23  	"fmt"
    24  	"log"
    25  	"os"
    26  	"os/exec"
    27  	"regexp"
    28  	"strings"
    29  
    30  	"golang.org/x/build/buildenv"
    31  	"golang.org/x/build/internal/envutil"
    32  )
    33  
    34  var (
    35  	prod    = flag.Bool("prod", false, "use production")
    36  	staging = flag.Bool("staging", false, "use staging")
    37  )
    38  
    39  func usage() {
    40  	fmt.Fprintf(os.Stderr, `xb [--prod or --staging] <CMD> [<ARGS>...]
    41  Example:
    42     xb --staging kubectl ...
    43     xb google-email
    44  `)
    45  	os.Exit(1)
    46  }
    47  
    48  func main() {
    49  	flag.Parse()
    50  	if flag.NArg() < 1 {
    51  		usage()
    52  	}
    53  
    54  	cmd := flag.Arg(0)
    55  	switch cmd {
    56  	case "kubectl":
    57  		env := getEnv()
    58  		curCtx := kubeCurrentContext()
    59  		wantCtx := fmt.Sprintf("gke_%s_%s_%s", env.ProjectName, env.KubeServices.Location(), env.KubeServices.Name)
    60  		if curCtx != wantCtx {
    61  			log.SetFlags(0)
    62  			log.Fatalf("Wrong kubectl context; currently using %q; want %q\nRun:\n  gcloud container clusters get-credentials --project=%s --zone=%s %s",
    63  				curCtx, wantCtx,
    64  				env.ProjectName, env.KubeServices.Location(), env.KubeServices.Name,
    65  			)
    66  		}
    67  		runCmd()
    68  	case "docker":
    69  		runDocker()
    70  	case "google-email":
    71  		out, err := exec.Command("gcloud", "config", "configurations", "list").CombinedOutput()
    72  		if err != nil {
    73  			log.Fatalf("gcloud: %v, %s", err, out)
    74  		}
    75  		googRx := regexp.MustCompile(`\S+@google\.com\b`)
    76  		e := googRx.FindString(string(out))
    77  		if e == "" {
    78  			log.Fatalf("didn't find @google.com address in gcloud config configurations list: %s", out)
    79  		}
    80  		fmt.Println(e)
    81  	default:
    82  		log.Fatalf("unknown command %q", cmd)
    83  	}
    84  }
    85  
    86  func kubeCurrentContext() string {
    87  	kubectl, err := exec.LookPath("kubectl")
    88  	if err != nil {
    89  		log.SetFlags(0)
    90  		log.Fatalf("No kubectl in path.")
    91  	}
    92  	// Get current context, but ignore errors, as kubectl returns an error
    93  	// if there's no context.
    94  	out, err := exec.Command(kubectl, "config", "current-context").Output()
    95  	if err != nil {
    96  		var stderr string
    97  		if ee, ok := err.(*exec.ExitError); ok {
    98  			stderr = string(ee.Stderr)
    99  		}
   100  		if strings.Contains(stderr, "current-context is not set") {
   101  			return ""
   102  		}
   103  		log.Printf("Failed to run 'kubectl config current-context': %v, %s", err, stderr)
   104  		return ""
   105  	}
   106  	return strings.TrimSpace(string(out))
   107  }
   108  
   109  func getEnv() *buildenv.Environment {
   110  	if *prod == *staging {
   111  		log.Fatalf("must specify exactly one of --prod or --staging")
   112  	}
   113  	if *prod {
   114  		return buildenv.Production
   115  	}
   116  	return buildenv.Staging
   117  }
   118  
   119  func runDocker() {
   120  	if flag.Arg(1) == "build" {
   121  		file := "Dockerfile"
   122  		for i, v := range flag.Args() {
   123  			if v == "-f" {
   124  				file = flag.Arg(i + 1)
   125  			}
   126  		}
   127  		layers := fromLayers(file)
   128  		for _, layer := range layers {
   129  			if strings.HasPrefix(layer, "golang:") ||
   130  				strings.HasPrefix(layer, "debian:") ||
   131  				strings.HasPrefix(layer, "arm32v6/debian:") ||
   132  				strings.HasPrefix(layer, "arm64v8/debian:") ||
   133  				strings.HasPrefix(layer, "alpine:") ||
   134  				strings.HasPrefix(layer, "fedora:") {
   135  				continue
   136  			}
   137  			switch layer {
   138  			case "golang/buildlet-stage0":
   139  				log.Printf("building dependent layer %q", layer)
   140  				buildStage0Container()
   141  			default:
   142  				log.Fatalf("unsupported layer %q; don't know how to validate or build", layer)
   143  			}
   144  		}
   145  	}
   146  
   147  	for i, v := range flag.Args() {
   148  		// Replace any occurrence of REPO with gcr.io/sybolic-datum-552 or
   149  		// the staging equivalent. Note that getEnv() is only called if
   150  		// REPO is already present, so the --prod and --staging flags
   151  		// aren't required to run "xb docker ..." in general.
   152  		if strings.Contains(v, "REPO") {
   153  			flag.Args()[i] = strings.Replace(v, "REPO", "gcr.io/"+getEnv().ProjectName, -1)
   154  		}
   155  	}
   156  
   157  	runCmd()
   158  }
   159  
   160  // fromLayers returns the layers named in the provided Dockerfile
   161  // file's FROM statements.
   162  func fromLayers(file string) (layers []string) {
   163  	f, err := os.Open(file)
   164  	if err != nil {
   165  		log.Fatal(err)
   166  	}
   167  	defer f.Close()
   168  	bs := bufio.NewScanner(f)
   169  	for bs.Scan() {
   170  		line := strings.TrimSpace(bs.Text())
   171  		if !strings.HasPrefix(line, "FROM") {
   172  			continue
   173  		}
   174  		f := strings.Fields(line)
   175  		if len(f) >= 2 && f[0] == "FROM" {
   176  			layers = append(layers, f[1])
   177  		}
   178  	}
   179  	if err := bs.Err(); err != nil {
   180  		log.Fatal(err)
   181  	}
   182  	return
   183  }
   184  
   185  func runCmd() {
   186  	cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
   187  	cmd.Stdout = os.Stdout
   188  	cmd.Stderr = os.Stderr
   189  	err := cmd.Run()
   190  	if err != nil {
   191  		// TODO: return with exact exit status? when needed.
   192  		log.Fatal(err)
   193  	}
   194  }
   195  
   196  func buildStage0Container() {
   197  	dir, err := exec.Command("go", "list", "-f", "{{.Dir}}", "golang.org/x/build/cmd/buildlet/stage0").Output()
   198  	if err != nil {
   199  		log.Fatalf("xb: error running go list to find golang.org/x/build/stage0: %v", err)
   200  	}
   201  
   202  	cmd := exec.Command("make", "docker")
   203  	envutil.SetDir(cmd, strings.TrimSpace(string(dir)))
   204  	cmd.Stdout = os.Stdout
   205  	cmd.Stderr = os.Stderr
   206  	if err := cmd.Run(); err != nil {
   207  		log.Fatal(err)
   208  	}
   209  }