| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // The stage0 command looks up the buildlet's URL from its environment |
| // (GCE metadata service, scaleway, etc), downloads it, and runs |
| // it. If not on GCE, such as when in a Linux Docker container being |
| // developed and tested locally, the stage0 instead looks for the |
| // META_BUILDLET_BINARY_URL environment to have a URL to the buildlet |
| // binary. |
| package main |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "net" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "time" |
| |
| "cloud.google.com/go/compute/metadata" |
| "golang.org/x/build/internal/httpdl" |
| "golang.org/x/build/internal/untar" |
| ) |
| |
| // This lets us be lazy and put the stage0 start-up in rc.local where |
| // it might race with the network coming up, rather than write proper |
| // upstart+systemd+init scripts: |
| var networkWait = flag.Duration("network-wait", 0, "if non-zero, the time to wait for the network to come up.") |
| |
| const osArch = runtime.GOOS + "/" + runtime.GOARCH |
| |
| const attr = "buildlet-binary-url" |
| |
| var ( |
| onScaleway bool |
| scalewayMeta scalewayMetadata |
| ) |
| |
| // untar helper, for the Windows image prep script. |
| var ( |
| untarFile = flag.String("untar-file", "", "if non-empty, tar.gz to untar to --untar-dest-dir") |
| untarDestDir = flag.String("untar-dest-dir", "", "destination directory to untar --untar-file to") |
| ) |
| |
| func main() { |
| flag.Parse() |
| |
| if *untarFile != "" { |
| untarMode() |
| return |
| } |
| |
| switch osArch { |
| case "linux/arm": |
| if os.Getenv("GO_BUILDER_ENV") == "linux-arm-arm5spacemonkey" { |
| // No setup currently. |
| } else { |
| if _, err := os.Stat("/usr/local/bin/oc-metadata"); err == nil { |
| initScaleway() |
| } |
| } |
| case "linux/arm64": |
| switch env := os.Getenv("GO_BUILDER_ENV"); env { |
| case "host-linux-arm64-packet", "host-linux-arm64-linaro": |
| // No special setup. |
| default: |
| panic(fmt.Sprintf("unknown/unspecified $GO_BUILDER_ENV value %q", env)) |
| } |
| case "linux/ppc64": |
| initOregonStatePPC64() |
| case "linux/ppc64le": |
| initOregonStatePPC64le() |
| } |
| |
| if !awaitNetwork() { |
| sleepFatalf("network didn't become reachable") |
| } |
| |
| // Note: we name it ".exe" for Windows, but the name also |
| // works fine on Linux, etc. |
| target := filepath.FromSlash("./buildlet.exe") |
| if err := download(target, buildletURL()); err != nil { |
| sleepFatalf("Downloading %s: %v", buildletURL, err) |
| } |
| |
| if runtime.GOOS != "windows" { |
| if err := os.Chmod(target, 0755); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| env := os.Environ() |
| if isUnix() && os.Getuid() == 0 { |
| if os.Getenv("USER") == "" { |
| env = append(env, "USER=root") |
| } |
| if os.Getenv("HOME") == "" { |
| env = append(env, "HOME=/root") |
| } |
| } |
| |
| cmd := exec.Command(target) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| cmd.Env = env |
| if onScaleway { |
| cmd.Args = append(cmd.Args, scalewayBuildletArgs()...) |
| } |
| if os.Getenv("GO_BUILDER_ENV") == "linux-arm-arm5spacemonkey" { |
| cmd.Args = append(cmd.Args, legacyReverseBuildletArgs("linux-arm-arm5spacemonkey")...) |
| } |
| switch osArch { |
| case "linux/s390x": |
| cmd.Args = append(cmd.Args, "--workdir=/data/golang/workdir") |
| cmd.Args = append(cmd.Args, legacyReverseBuildletArgs("linux-s390x-ibm")...) |
| case "linux/arm64": |
| switch v := os.Getenv("GO_BUILDER_ENV"); v { |
| case "host-linux-arm64-packet", "host-linux-arm64-linaro": |
| hostname := os.Getenv("HOSTNAME") // if empty, docker container name is used |
| cmd.Args = append(cmd.Args, |
| "--reverse-type="+v, |
| "--workdir=/workdir", |
| "--hostname="+hostname, |
| "--halt=false", |
| "--reboot=false", |
| "--coordinator=farmer.golang.org:443", |
| ) |
| default: |
| panic(fmt.Sprintf("unknown/unspecified $GO_BUILDER_ENV value %q", env)) |
| } |
| case "linux/ppc64": |
| cmd.Args = append(cmd.Args, legacyReverseBuildletArgs("linux-ppc64-buildlet")...) |
| case "linux/ppc64le": |
| cmd.Args = append(cmd.Args, legacyReverseBuildletArgs("linux-ppc64le-buildlet")...) |
| case "solaris/amd64": |
| cmd.Args = append(cmd.Args, legacyReverseBuildletArgs("solaris-amd64-smartosbuildlet")...) |
| } |
| if err := cmd.Run(); err != nil { |
| sleepFatalf("Error running buildlet: %v", err) |
| } |
| } |
| |
| // legacyReverseBuildletArgs passes builder as the deprecated --reverse flag. |
| // New code should use --reverse-type instead. |
| func legacyReverseBuildletArgs(builder string) []string { |
| return []string{ |
| "--halt=false", |
| "--reverse=" + builder, |
| "--coordinator=farmer.golang.org:443", |
| } |
| } |
| |
| func scalewayBuildletArgs() []string { |
| var modes []string // e.g. "linux-arm", "linux-arm-arm5" |
| // tags are of form "buildkey_linux-arm_HEXHEXHEX" |
| for _, tag := range scalewayMeta.Tags { |
| if strings.HasPrefix(tag, "buildkey_") { |
| parts := strings.Split(tag, "_") |
| if len(parts) != 3 { |
| log.Fatalf("invalid server tag %q", tag) |
| } |
| mode, buildkey := parts[1], parts[2] |
| modes = append(modes, mode) |
| file := "/root/.gobuildkey-" + mode |
| if fi, err := os.Stat(file); err != nil || (err == nil && fi.Size() == 0) { |
| if err := ioutil.WriteFile(file, []byte(buildkey), 0600); err != nil { |
| log.Fatal(err) |
| } |
| } |
| } |
| } |
| server := "farmer.golang.org:443" |
| if scalewayMeta.IsStaging() { |
| server = "104.154.113.235:443" // fixed IP, but no hostname. |
| } |
| return []string{ |
| "--workdir=/workdir", |
| "--hostname=" + scalewayMeta.Hostname, |
| "--halt=false", |
| "--reverse=" + strings.Join(modes, ","), |
| "--coordinator=" + server, |
| } |
| } |
| |
| // awaitNetwork reports whether the network came up within 30 seconds, |
| // determined somewhat arbitrarily via a DNS lookup for google.com. |
| func awaitNetwork() bool { |
| for deadline := time.Now().Add(30 * time.Second); time.Now().Before(deadline); time.Sleep(time.Second) { |
| if addrs, _ := net.LookupIP("google.com"); len(addrs) > 0 { |
| log.Printf("network is up.") |
| return true |
| } |
| log.Printf("waiting for network...") |
| } |
| log.Printf("gave up waiting for network") |
| return false |
| } |
| |
| func buildletURL() string { |
| if os.Getenv("GO_BUILDER_ENV") == "linux-arm-arm5spacemonkey" { |
| return "https://storage.googleapis.com/go-builder-data/buildlet.linux-arm-arm5" |
| } |
| switch osArch { |
| case "linux/s390x": |
| return "https://storage.googleapis.com/go-builder-data/buildlet.linux-s390x" |
| case "linux/arm64": |
| return "https://storage.googleapis.com/go-builder-data/buildlet.linux-arm64" |
| case "linux/ppc64": |
| return "https://storage.googleapis.com/go-builder-data/buildlet.linux-ppc64" |
| case "linux/ppc64le": |
| return "https://storage.googleapis.com/go-builder-data/buildlet.linux-ppc64le" |
| case "solaris/amd64": |
| return "https://storage.googleapis.com/go-builder-data/buildlet.solaris-amd64" |
| } |
| // The buildlet download URL is located in an env var |
| // when the buildlet is not running on GCE, or is running |
| // on Kubernetes. |
| if !metadata.OnGCE() || os.Getenv("IN_KUBERNETES") == "1" { |
| if v := os.Getenv("META_BUILDLET_BINARY_URL"); v != "" { |
| return v |
| } |
| if onScaleway { |
| if scalewayMeta.IsStaging() { |
| return "https://storage.googleapis.com/dev-go-builder-data/buildlet.linux-arm" |
| } else { |
| return "https://storage.googleapis.com/go-builder-data/buildlet.linux-arm" |
| } |
| } |
| sleepFatalf("Not on GCE, and no META_BUILDLET_BINARY_URL specified.") |
| } |
| v, err := metadata.InstanceAttributeValue(attr) |
| if err != nil { |
| sleepFatalf("Failed to look up %q attribute value: %v", attr, err) |
| } |
| return v |
| } |
| |
| func sleepFatalf(format string, args ...interface{}) { |
| log.Printf(format, args...) |
| if runtime.GOOS == "windows" { |
| log.Printf("(sleeping for 1 minute before failing)") |
| time.Sleep(time.Minute) // so user has time to see it in cmd.exe, maybe |
| } |
| os.Exit(1) |
| } |
| |
| func download(file, url string) error { |
| log.Printf("Downloading %s to %s ...\n", url, file) |
| deadline := time.Now().Add(*networkWait) |
| for { |
| err := httpdl.Download(file, url) |
| if err == nil { |
| fi, _ := os.Stat(file) |
| log.Printf("Downloaded %s (%d bytes)", file, fi.Size()) |
| return nil |
| } |
| // TODO(bradfitz): delete this whole download function |
| // and move this functionality into a "WaitNetwork" |
| // function somewhere? |
| if time.Now().Before(deadline) { |
| time.Sleep(1 * time.Second) |
| continue |
| } |
| return err |
| } |
| } |
| |
| func initScaleway() { |
| log.Printf("On scaleway.") |
| onScaleway = true |
| initScalewaySwap() |
| initScalewayWorkdir() |
| initScalewayMeta() |
| initScalewayDNS() |
| initScalewayGo14() |
| log.Printf("Scaleway init complete; metadata is %+v", scalewayMeta) |
| } |
| |
| type scalewayMetadata struct { |
| Name string `json:"name"` |
| Hostname string `json:"hostname"` |
| Tags []string `json:"tags"` |
| } |
| |
| // IsStaging reports whether this instance has a "staging" tag. |
| func (m *scalewayMetadata) IsStaging() bool { |
| for _, t := range m.Tags { |
| if t == "staging" { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func initScalewayMeta() { |
| const metaURL = "http://169.254.42.42/conf?format=json" |
| res, err := http.Get(metaURL) |
| if err != nil { |
| log.Fatalf("failed to get scaleway metadata: %v", err) |
| } |
| defer res.Body.Close() |
| if res.StatusCode != http.StatusOK { |
| log.Fatalf("failed to get scaleway metadata from %s: %v", metaURL, res.Status) |
| } |
| if err := json.NewDecoder(res.Body).Decode(&scalewayMeta); err != nil { |
| log.Fatalf("invalid JSON from scaleway metadata URL %s: %v", metaURL, err) |
| } |
| } |
| |
| func initScalewayDNS() { |
| setFileContents("/etc/resolv.conf", []byte("nameserver 8.8.8.8\n")) |
| } |
| |
| func setFileContents(file string, contents []byte) { |
| old, err := ioutil.ReadFile(file) |
| if err == nil && bytes.Equal(old, contents) { |
| return |
| } |
| if err := ioutil.WriteFile(file, contents, 0644); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| func initScalewaySwap() { |
| const swapFile = "/swapfile" |
| slurp, _ := ioutil.ReadFile("/proc/swaps") |
| if strings.Contains(string(slurp), swapFile) { |
| log.Printf("scaleway swapfile already active.") |
| return |
| } |
| os.Remove(swapFile) // if it already exists, else ignore error |
| log.Printf("Running fallocate on swapfile") |
| if out, err := exec.Command("fallocate", "--length", "16GiB", swapFile).CombinedOutput(); err != nil { |
| log.Fatalf("Failed to fallocate /swapfile: %v, %s", err, out) |
| } |
| log.Printf("Running mkswap") |
| if out, err := exec.Command("mkswap", swapFile).CombinedOutput(); err != nil { |
| log.Fatalf("Failed to mkswap /swapfile: %v, %s", err, out) |
| } |
| os.Chmod(swapFile, 0600) |
| log.Printf("Running swapon") |
| if out, err := exec.Command("swapon", swapFile).CombinedOutput(); err != nil { |
| log.Fatalf("Failed to swapon /swapfile: %v, %s", err, out) |
| } |
| } |
| |
| func initScalewayWorkdir() { |
| const dir = "/workdir" |
| slurp, _ := ioutil.ReadFile("/proc/mounts") |
| if strings.Contains(string(slurp), dir) { |
| log.Printf("scaleway workdir already mounted") |
| return |
| } |
| if err := os.MkdirAll("/workdir", 0755); err != nil { |
| log.Fatal(err) |
| } |
| if out, err := exec.Command("mount", |
| "-t", "tmpfs", |
| "-o", "size=8589934592", |
| "tmpfs", "/workdir").CombinedOutput(); err != nil { |
| log.Fatalf("Failed to mount /buildtmp: %v, %s", err, out) |
| } |
| } |
| |
| func initScalewayGo14() { |
| if fi, err := os.Stat("/usr/local/go"); err == nil && fi.IsDir() { |
| log.Printf("go directory already exists.") |
| return |
| } |
| os.RemoveAll("/usr/local/go") // in case it existed somehow, or as regular file |
| if err := os.RemoveAll("/usr/local/go.tmp"); err != nil { |
| log.Fatal(err) |
| } |
| if err := os.MkdirAll("/usr/local/go.tmp", 0755); err != nil { |
| log.Fatal(err) |
| } |
| log.Printf("Downloading go1.4-linux-arm.tar.gz") |
| if out, err := exec.Command("curl", |
| "-o", "/usr/local/go.tmp/go.tar.gz", |
| "--silent", |
| "https://storage.googleapis.com/go-builder-data/go1.4-linux-arm.tar.gz", |
| ).CombinedOutput(); err != nil { |
| log.Fatalf("Failed to download go1.4-linux-arm.tar.gz: %v, %s", err, out) |
| } |
| log.Printf("Extracting go1.4-linux-arm.tar.gz") |
| if out, err := exec.Command("tar", |
| "-C", "/usr/local/go.tmp", |
| "-zx", |
| "-f", "/usr/local/go.tmp/go.tar.gz", |
| ).CombinedOutput(); err != nil { |
| log.Fatalf("Failed to untar go1.4-linux-arm.tar.gz: %v, %s", err, out) |
| } |
| if err := os.Rename("/usr/local/go.tmp", "/usr/local/go"); err != nil { |
| log.Fatal(err) |
| } |
| } |
| |
| func aptGetInstall(pkgs ...string) { |
| args := append([]string{"--yes", "install"}, pkgs...) |
| cmd := exec.Command("apt-get", args...) |
| if out, err := cmd.CombinedOutput(); err != nil { |
| log.Fatalf("error running apt-get install: %s", out) |
| } |
| } |
| |
| func initBootstrapDir(destDir, tgzCache string) { |
| if err := os.MkdirAll(destDir, 0755); err != nil { |
| log.Fatal(err) |
| } |
| // TODO(bradfitz): rewrite this to use Go instead of curl+tar |
| // if this ever gets used on platforms besides Unix. For |
| // Windows and Plan 9 we bake in the bootstrap tarball into |
| // the image anyway. So this works for now. Solaris might require |
| // tweaking to use gtar instead or something. |
| latestURL := fmt.Sprintf("https://storage.googleapis.com/go-builder-data/gobootstrap-%s-%s.tar.gz", |
| runtime.GOOS, runtime.GOARCH) |
| curl := exec.Command("/usr/bin/curl", "-R", "-o", tgzCache, "-z", tgzCache, latestURL) |
| out, err := curl.CombinedOutput() |
| if err != nil { |
| log.Fatalf("curl error fetching %s to %s: %s", latestURL, out, err) |
| } |
| tar := exec.Command("tar", "zxf", tgzCache) |
| tar.Dir = destDir |
| out, err = tar.CombinedOutput() |
| if err != nil { |
| log.Fatalf("error untarring %s to %s: %s", tgzCache, destDir, out) |
| } |
| } |
| |
| func initOregonStatePPC64() { |
| aptGetInstall("gcc", "strace", "libc6-dev", "gdb") |
| initBootstrapDir("/usr/local/go-bootstrap", "/usr/local/go-bootstrap.tar.gz") |
| } |
| |
| func initOregonStatePPC64le() { |
| aptGetInstall("gcc", "strace", "libc6-dev", "gdb") |
| initBootstrapDir("/usr/local/go-bootstrap", "/usr/local/go-bootstrap.tar.gz") |
| } |
| |
| func isUnix() bool { |
| switch runtime.GOOS { |
| case "plan9", "windows": |
| return false |
| } |
| return true |
| } |
| |
| func untarMode() { |
| if *untarDestDir == "" { |
| log.Fatal("--untar-dest-dir must not be empty") |
| } |
| if fi, err := os.Stat(*untarDestDir); err != nil || !fi.IsDir() { |
| if err != nil { |
| log.Fatalf("--untar-dest-dir %q: %v", *untarDestDir, err) |
| } |
| log.Fatalf("--untar-dest-dir %q not a directory.", *untarDestDir) |
| } |
| f, err := os.Open(*untarFile) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer f.Close() |
| if err := untar.Untar(f, *untarDestDir); err != nil { |
| log.Fatalf("Untarring %q to %q: %v", *untarFile, *untarDestDir, err) |
| } |
| } |