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 }