diff --git a/cmd/buildlet/buildlet.go b/cmd/buildlet/buildlet.go index 222b5c8850..ae10490879 100644 --- a/cmd/buildlet/buildlet.go +++ b/cmd/buildlet/buildlet.go @@ -1337,14 +1337,27 @@ func handleConnectSSH(w http.ResponseWriter, r *http.Request) { } } - sshConn, err := net.Dial("tcp", "localhost:"+sshPort()) - if err != nil { - sshServerOnce.Do(startSSHServer) + sshServerOnce.Do(startSSHServer) + + var sshConn net.Conn + var err error + + // In theory we shouldn't need retries here at all, but the + // startSSHServerLinux's use of sshd -D is kinda sketchy and + // restarts the process whenever we connect to it, so in case + // it's just down between restarts, try a few times. 5 tries + // and 5 seconds seems plenty. + const maxTries = 5 + for try := 1; try <= maxTries; try++ { sshConn, err = net.Dial("tcp", "localhost:"+sshPort()) - if err != nil { + if err == nil { + break + } + if try == maxTries { http.Error(w, err.Error(), http.StatusBadGateway) return } + time.Sleep(time.Second) } defer sshConn.Close() hj, ok := w.(http.Hijacker) @@ -1450,13 +1463,27 @@ func startSSHServerLinux() { } } - cmd := exec.Command("/usr/sbin/sshd", "-D", "-p", sshPort()) - err := cmd.Start() - if err != nil { - log.Printf("starting sshd: %v", err) - return - } - log.Printf("sshd started.") + go func() { + for { + // TODO: using sshd -D isn't great as it only + // handles a single connection and exits. + // Maybe run in sshd -i (inetd) mode instead, + // and hook that up to the buildlet directly? + t0 := time.Now() + cmd := exec.Command("/usr/sbin/sshd", "-D", "-p", sshPort(), "-d", "-d") + cmd.Stderr = os.Stderr + err := cmd.Start() + if err != nil { + log.Printf("starting sshd: %v", err) + return + } + log.Printf("sshd started.") + log.Printf("sshd exited: %v; restarting", cmd.Wait()) + if d := time.Since(t0); d < time.Second { + time.Sleep(time.Second - d) + } + } + }() waitLocalSSH() } diff --git a/cmd/buildlet/stage0/Dockerfile b/cmd/buildlet/stage0/Dockerfile index 579cd398f1..149e2439ba 100644 --- a/cmd/buildlet/stage0/Dockerfile +++ b/cmd/buildlet/stage0/Dockerfile @@ -2,7 +2,7 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -FROM golang:1.12 AS build +FROM golang:1.13 AS build LABEL maintainer "golang-dev@googlegroups.com" ENV GO111MODULE=on @@ -24,6 +24,8 @@ COPY . /go/src/golang.org/x/build/ # Install binary to /go/bin/stage0 RUN go install golang.org/x/build/cmd/buildlet/stage0 +RUN CGO_ENABLED=0 go build -o /go/bin/stage0.static golang.org/x/build/cmd/buildlet/stage0 -FROM golang:1.12 +FROM golang:1.13 COPY --from=build /go/bin/stage0 /go/bin/stage0 +COPY --from=build /go/bin/stage0.static /go/bin/stage0.static diff --git a/cmd/buildlet/testssh/testssh.go b/cmd/buildlet/testssh/testssh.go new file mode 100644 index 0000000000..7761085017 --- /dev/null +++ b/cmd/buildlet/testssh/testssh.go @@ -0,0 +1,165 @@ +// Copyright 2019 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 testssh binary exists to verify that a buildlet container's +// ssh works, without running the whole coordinator binary in the +// staging environment. +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "golang.org/x/build/buildenv" + "golang.org/x/build/buildlet" +) + +var ( + container = flag.String("container", "", "if non-empty, the ID of a running docker container") + startImage = flag.String("start-image", "", "if non-empty, the Docker image to start a buildlet of locally, and use its container ID for the -container value") + user = flag.String("user", "root", "SSH user") +) + +func main() { + flag.Parse() + ipPort := getIPPort() + defer cleanContainer() + + bc := buildlet.NewClient(ipPort, buildlet.NoKeyPair) + for { + c, err := net.Dial("tcp", ipPort) + if err == nil { + c.Close() + break + } + log.Printf("waiting for %v to come up...", ipPort) + time.Sleep(time.Second) + } + + pubKey, privPath := genKey() + + log.Printf("hitting buildlet's /connect-ssh ...") + buildletConn, err := bc.ConnectSSH(*user, pubKey) + if err != nil { + var out []byte + if *container != "" { + var err error + out, err = exec.Command("docker", "logs", *container).CombinedOutput() + if err != nil { + log.Printf("failed to fetch docker logs: %v", err) + } + } + cleanContainer() + log.Printf("image logs: %s", out) + log.Fatalf("ConnectSSH: %v (logs above)", err) + } + defer buildletConn.Close() + log.Printf("ConnectSSH succeeded; testing connection...") + + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { + log.Fatal(err) + } + go func() { + c, err := ln.Accept() + if err != nil { + log.Fatal(err) + } + go io.Copy(buildletConn, c) + go io.Copy(c, buildletConn) + }() + ip, port, err := net.SplitHostPort(ln.Addr().String()) + if err != nil { + log.Fatal(err) + } + + cmd := exec.Command("ssh", + "-v", + "-i", privPath, + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "LogLevel=ERROR", + "-p", port, + *user+"@"+ip, + "echo", "SSH works") + stdout := new(bytes.Buffer) + stderr := new(bytes.Buffer) + cmd.Stderr = stderr + cmd.Stdout = stdout + if err := cmd.Run(); err != nil { + cleanContainer() + log.Fatalf("ssh client: %v, %s", err, stderr) + } + fmt.Print(stdout.String()) +} + +func cleanContainer() { + if *startImage == "" { + return + } + out, err := exec.Command("docker", "rm", "-f", *container).CombinedOutput() + if err != nil { + log.Printf("docker rm: %v, %s", err, out) + } +} + +func genKey() (pubKey, privateKeyPath string) { + cache, err := os.UserCacheDir() + if err != nil { + log.Fatal(err) + } + cache = filepath.Join(cache, "testssh") + os.MkdirAll(cache, 0755) + privateKeyPath = filepath.Join(cache, "testkey") + pubKeyPath := filepath.Join(cache, "testkey.pub") + if _, err := os.Stat(pubKeyPath); err != nil { + out, err := exec.Command("ssh-keygen", "-t", "ed25519", "-f", privateKeyPath, "-N", "").CombinedOutput() + if err != nil { + log.Fatalf("ssh-keygen: %v, %s", err, out) + } + } + slurp, err := ioutil.ReadFile(pubKeyPath) + if err != nil { + log.Fatal(err) + } + return strings.TrimSpace(string(slurp)), privateKeyPath +} + +func getIPPort() string { + if *startImage != "" { + buildlet := "buildlet.linux-amd64" + if strings.Contains(*startImage, "linux-x86-alpine") { + buildlet = "buildlet.linux-amd64-static" + } + log.Printf("creating container with image %s ...", *startImage) + out, err := exec.Command("docker", "run", "-d", + "--stop-timeout=300", + "-e", "META_BUILDLET_BINARY_URL=https://storage.googleapis.com/"+buildenv.Production.BuildletBucket+"/"+buildlet, + *startImage).CombinedOutput() + if err != nil { + log.Fatalf("docker run: %v, %s", err, out) + } + *container = strings.TrimSpace(string(out)) + log.Printf("created container %s ...", *container) + } + if *container != "" { + out, err := exec.Command("bash", "-c", "docker inspect "+*container+" | jq -r '.[0].NetworkSettings.IPAddress'").CombinedOutput() + if err != nil { + log.Fatalf("%v: %s", err, out) + } + return strings.TrimSpace(string(out)) + ":80" + } + log.Fatalf("no address specified") + return "" +} diff --git a/cmd/xb/xb.go b/cmd/xb/xb.go index e976ce0cf9..238232d584 100644 --- a/cmd/xb/xb.go +++ b/cmd/xb/xb.go @@ -130,6 +130,7 @@ func runDocker() { for _, layer := range layers { if strings.HasPrefix(layer, "golang:") || strings.HasPrefix(layer, "debian:") || + strings.HasPrefix(layer, "alpine:") || strings.HasPrefix(layer, "fedora:") { continue } diff --git a/dashboard/builders.go b/dashboard/builders.go index 5209eb6773..a218248cdf 100644 --- a/dashboard/builders.go +++ b/dashboard/builders.go @@ -167,6 +167,7 @@ var Hosts = map[string]*HostConfig{ ContainerImage: "js-wasm:latest", buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64", env: []string{"GOROOT_BOOTSTRAP=/go1.4"}, + SSHUsername: "root", }, "host-s390x-cross-kube": &HostConfig{ Notes: "Container with s390x cross-compiler.", @@ -177,26 +178,30 @@ var Hosts = map[string]*HostConfig{ "host-linux-x86-alpine": &HostConfig{ Notes: "Alpine container", ContainerImage: "linux-x86-alpine:latest", - buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64-static", + buildletURLTmpl: "https://storage.googleapis.com/$BUCKET/buildlet.linux-amd64-static", env: []string{"GOROOT_BOOTSTRAP=/usr/lib/go"}, + SSHUsername: "root", }, "host-linux-clang": &HostConfig{ Notes: "Container with clang.", ContainerImage: "linux-x86-clang:latest", buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64", env: []string{"GOROOT_BOOTSTRAP=/go1.4"}, + SSHUsername: "root", }, "host-linux-sid": &HostConfig{ Notes: "Debian sid, updated occasionally.", ContainerImage: "linux-x86-sid:latest", buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64", env: []string{"GOROOT_BOOTSTRAP=/go1.4"}, + SSHUsername: "root", }, "host-linux-fedora": &HostConfig{ Notes: "Fedora 30", ContainerImage: "linux-x86-fedora:latest", buildletURLTmpl: "http://storage.googleapis.com/$BUCKET/buildlet.linux-amd64", env: []string{"GOROOT_BOOTSTRAP=/goboot"}, + SSHUsername: "root", }, "host-linux-arm-scaleway": &HostConfig{ IsReverse: true, diff --git a/env/linux-x86-alpine/Dockerfile b/env/linux-x86-alpine/Dockerfile index 99e1926d13..a2637c3cd4 100644 --- a/env/linux-x86-alpine/Dockerfile +++ b/env/linux-x86-alpine/Dockerfile @@ -8,7 +8,7 @@ FROM golang/buildlet-stage0 AS stage0 -FROM alpine:3.5 +FROM alpine:3.10 MAINTAINER golang-dev RUN apk add --no-cache \ @@ -24,10 +24,15 @@ RUN apk add --no-cache \ go \ libc-dev \ lsof \ + openssh \ procps \ strace -COPY --from=stage0 /go/bin/stage0 /usr/local/bin/stage0 +RUN ssh-keygen -A +RUN bash -c "(echo ChallengeResponseAuthentication no; echo PasswordAuthentication no; echo PermitRootLogin yes) > /etc/ssh/sshd_config" +RUN passwd -u root + +COPY --from=stage0 /go/bin/stage0.static /usr/local/bin/stage0 ENV GOROOT_BOOTSTRAP=/usr/lib/go diff --git a/env/linux-x86-clang/Dockerfile b/env/linux-x86-clang/Dockerfile index 70d54147d0..861b368a80 100644 --- a/env/linux-x86-clang/Dockerfile +++ b/env/linux-x86-clang/Dockerfile @@ -2,21 +2,16 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. -# Linux builder VM with clang instead of gccc. +# Linux builder VM with clang instead of gcc. # Docker tag gobuilders/linux-x86-clang FROM golang/buildlet-stage0 AS stage0 -FROM debian:jessie +FROM debian:buster MAINTAINER golang-dev ENV DEBIAN_FRONTEND noninteractive -COPY sources/clang-deps.list /etc/apt/sources.list.d/ -COPY llvm-snapshot.gpg.key /tmp/ - -RUN apt-key add /tmp/llvm-snapshot.gpg.key - # strace: optionally used by some net/http tests # libc6-dev-i386 gcc-multilib: for 32-bit builds # procps lsof psmisc: misc basic tools @@ -24,17 +19,18 @@ RUN apt-get update && apt-get install -y \ --no-install-recommends \ ca-certificates \ curl \ - clang-3.9 \ + clang \ strace \ libc6-dev-i386 \ gcc-multilib \ procps \ lsof \ psmisc \ + openssh-server \ && rm -rf /var/lib/apt/lists/* \ - && rm -f /usr/bin/gcc \ - && ln -snf /usr/bin/clang-3.9 /usr/bin/clang \ - && ln -snf /usr/bin/clang++-3.9 /usr/bin/clang++ + && rm -f /usr/bin/gcc + +RUN mkdir --mode=0700 /run/sshd RUN mkdir -p /go1.4-amd64 \ && ( \ diff --git a/env/linux-x86-fedora/Dockerfile b/env/linux-x86-fedora/Dockerfile index bf1ea62d1e..b62b18a77e 100644 --- a/env/linux-x86-fedora/Dockerfile +++ b/env/linux-x86-fedora/Dockerfile @@ -15,11 +15,14 @@ RUN yum -y update && yum -y install \ patch \ strace \ which \ + openssh-server \ && true +RUN ssh-keygen -A + RUN mkdir -p /goboot-amd64 \ && ( \ - curl --silent https://storage.googleapis.com/golang/go1.12.5.linux-amd64.tar.gz | tar -C /goboot-amd64 -zxv \ + curl --silent https://storage.googleapis.com/golang/go1.13.4.linux-amd64.tar.gz | tar -C /goboot-amd64 -zxv \ ) \ && mv /goboot-amd64/go /goboot \ && rm -rf /goboot-amd64 \ diff --git a/env/linux-x86-sid/Dockerfile b/env/linux-x86-sid/Dockerfile index b2cdd3996d..7854ab2c26 100644 --- a/env/linux-x86-sid/Dockerfile +++ b/env/linux-x86-sid/Dockerfile @@ -4,7 +4,7 @@ FROM golang/buildlet-stage0 AS stage0 -FROM debian:sid-20190326 +FROM debian:sid-20191014 MAINTAINER golang-dev ENV DEBIAN_FRONTEND noninteractive @@ -39,11 +39,15 @@ RUN apt-get update && apt-get install -y \ upx \ qemu-user \ netbase \ + openssh-server \ && rm -rf /var/lib/apt/lists/* +# Otherwise we get "Missing privilege separation directory: /run/sshd" ... +RUN mkdir --mode=0700 /run/sshd + RUN mkdir -p /go1.4-amd64 \ && ( \ - curl --silent https://storage.googleapis.com/golang/go1.4.linux-amd64.tar.gz | tar -C /go1.4-amd64 -zxv \ + curl --silent https://storage.googleapis.com/golang/go1.4.3.linux-amd64.tar.gz | tar -C /go1.4-amd64 -zxv \ ) \ && mv /go1.4-amd64/go /go1.4 \ && rm -rf /go1.4-amd64 \ diff --git a/env/shared-makefile/Makefile b/env/shared-makefile/Makefile index 86eb93c217..2c7f825969 100644 --- a/env/shared-makefile/Makefile +++ b/env/shared-makefile/Makefile @@ -11,6 +11,10 @@ docker: Dockerfile go install golang.org/x/build/cmd/xb xb docker build -t golang/$(IMAGE_NAME) . +testssh: docker + go install golang.org/x/build/cmd/buildlet/testssh + testssh --start-image=golang/$(IMAGE_NAME) + push-staging: docker go install golang.org/x/build/cmd/xb xb --staging docker tag golang/$(IMAGE_NAME) REPO/$(IMAGE_NAME):latest