From 0757b4eacee46aad9fd6cfbe2ec359fbd92cd316 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Fri, 17 Aug 2018 19:58:32 -0400 Subject: [PATCH 001/148] playground: serve 404 for non-existent pages The server.handleEdit handler is being registered to handle the "/" pattern. Its code checks if the request URL path has a "/p/" prefix, and otherwise assumes it's the index page. There's no check if it's some other unsupported URL. As a result, any URL that isn't handled elsewhere serves the index page. For example: https://play.golang.org/foobarbaz This change fixes that so non-existent pages return a 404 Not Found error instead. Also use context.Background() instead of a nil context in a test, per the context package instructions. Change-Id: I4c43492397a6f71bffc1e6a657ff2a523a245f94 Reviewed-on: https://go-review.googlesource.com/129795 Reviewed-by: Brad Fitzpatrick --- edit.go | 6 ++++++ server_test.go | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/edit.go b/edit.go index b69c1c0f..81923e4a 100644 --- a/edit.go +++ b/edit.go @@ -32,6 +32,12 @@ func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) { return } + // Serve 404 for /foo. + if r.URL.Path != "/" && !strings.HasPrefix(r.URL.Path, "/p/") { + http.NotFound(w, r) + return + } + snip := &snippet{Body: []byte(hello)} if strings.HasPrefix(r.URL.Path, "/p/") { if !allowShare(r) { diff --git a/server_test.go b/server_test.go index 0c174075..86217424 100644 --- a/server_test.go +++ b/server_test.go @@ -5,6 +5,7 @@ package main import ( "bytes" + "context" "fmt" "io/ioutil" "net/http" @@ -42,8 +43,8 @@ func TestEdit(t *testing.T) { id := "bar" barBody := []byte("Snippy McSnipface") snip := &snippet{Body: barBody} - if err := s.db.PutSnippet(nil, id, snip); err != nil { - t.Fatalf("s.dbPutSnippet(nil, %+v, %+v): %v", id, snip, err) + if err := s.db.PutSnippet(context.Background(), id, snip); err != nil { + t.Fatalf("s.dbPutSnippet(context.Background(), %+v, %+v): %v", id, snip, err) } testCases := []struct { @@ -54,6 +55,7 @@ func TestEdit(t *testing.T) { respBody []byte }{ {"foo.play.golang.org to play.golang.org", "https://foo.play.golang.org", http.StatusFound, map[string]string{"Location": "https://play.golang.org"}, nil}, + {"Non-existent page", "https://play.golang.org/foo", http.StatusNotFound, nil, nil}, {"Unknown snippet", "https://play.golang.org/p/foo", http.StatusNotFound, nil, nil}, {"Existing snippet", "https://play.golang.org/p/" + id, http.StatusOK, nil, nil}, {"Plaintext snippet", "https://play.golang.org/p/" + id + ".go", http.StatusOK, nil, barBody}, From e07747d8d2cee6639b14ba8021d453a6b8dd9edc Mon Sep 17 00:00:00 2001 From: Katie Hockman Date: Mon, 20 Aug 2018 15:55:28 -0400 Subject: [PATCH 002/148] playground: prevent caching memory-related runtime errors The existing implementation only prevents code caching if the code has certain compile-time errors. Code with deterministic runtime errors, such as out of memory errors, are being cached. This causes errors to be returned in the Go Tour if a different user's run caused an error. The cache had to be manually cleared in order to clean out the bad run. This change prevents code that threw an "out of memory" or a "cannot allocate memory" error from being cached. Fixes golang/go#26926 Change-Id: Ib281f3076bc674e7c2f08bf9b5c4be36da22d28e Reviewed-on: https://go-review.googlesource.com/130035 Reviewed-by: Andrew Bonventre --- sandbox.go | 15 +++++++++++++++ server_test.go | 13 +++++++++++++ 2 files changed, 28 insertions(+) diff --git a/sandbox.go b/sandbox.go index 36d2f28f..f63c360a 100644 --- a/sandbox.go +++ b/sandbox.go @@ -41,6 +41,9 @@ const ( progName = "prog.go" ) +// Runtime error strings that will not be cached if found in an Event message. +var nonCachingRuntimeErrors = []string{"out of memory", "cannot allocate memory"} + type request struct { Body string } @@ -87,6 +90,18 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*res http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } + for _, el := range resp.Events { + if el.Kind != "stderr" { + continue + } + for _, e := range nonCachingRuntimeErrors { + if strings.Contains(el.Message, e) { + s.log.Errorf("cmdFunc runtime error: %q", el.Message) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } + } if err := s.cache.Set(key, resp); err != nil { s.log.Errorf("cache.Set(%q, resp): %v", key, err) } diff --git a/server_test.go b/server_test.go index 86217424..63141930 100644 --- a/server_test.go +++ b/server_test.go @@ -166,6 +166,15 @@ func TestCommandHandler(t *testing.T) { if r.Body == "error" { return &response{Errors: "errors"}, nil } + if r.Body == "oom-error" { + // To throw an oom in a local playground instance, increase the server timeout + // to 20 seconds (within sandbox.go), spin up the Docker instance and run + // this code: https://play.golang.org/p/aaCv86m0P14. + return &response{Events: []Event{{"out of memory", "stderr", 0}}}, nil + } + if r.Body == "allocate-memory-error" { + return &response{Events: []Event{{"cannot allocate memory", "stderr", 0}}}, nil + } resp := &response{Events: []Event{{r.Body, "stdout", 0}}} return resp, nil }) @@ -191,6 +200,10 @@ func TestCommandHandler(t *testing.T) { []byte(`{"Errors":"errors","Events":null} `), }, + {"Out of memory error in response body event message", http.MethodPost, http.StatusInternalServerError, + []byte(`{"Body":"oom-error"}`), nil}, + {"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError, + []byte(`{"Body":"allocate-memory-error"}`), nil}, } for _, tc := range testCases { From 132c64fbd49b49d3fb4fa41f9fe270fe08510ebb Mon Sep 17 00:00:00 2001 From: Yury Smolsky Date: Fri, 5 Oct 2018 14:35:49 +0300 Subject: [PATCH 003/148] Dockerfile: use latest x/tools containing the CL 113875 Use the godoc with the CL 113875. That CL handles the case when there was no output from the user program and vet returned an error. Fixes golang/go#27980 Change-Id: I8f3d5ff477fb930c099fd9780c102c8421403e55 Reviewed-on: https://go-review.googlesource.com/c/140017 Reviewed-by: Dmitri Shuralyov Reviewed-by: Brad Fitzpatrick --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 41a0849a..6e3d1386 100644 --- a/Dockerfile +++ b/Dockerfile @@ -72,8 +72,8 @@ ENV REV=b7ef84aaf62aa3e70962625c80a571ae7c17cb40 RUN go get -d golang.org/x/text/secure/bidirule `#and 4 other pkgs` &&\ (cd /go/src/golang.org/x/text && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo golang.org/x/tools at 48418e5 (2018-05-08) -ENV REV=48418e5732e1b1e2a10207c8007a5f959e422f20 +# Repo golang.org/x/tools at 59602fd (2018-10-04) +ENV REV=59602fdee893255faef9b2cd3aa8f880ea85265c RUN go get -d golang.org/x/tools/go/ast/astutil `#and 3 other pkgs` &&\ (cd /go/src/golang.org/x/tools && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) From e11e51b7c6c209220dd83a3a46cebd7bd49298a9 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Fri, 5 Oct 2018 11:07:09 -0400 Subject: [PATCH 004/148] playground: use Go 1.11.1 Go 1.11.1 has been released and should be used. Revert CL 106216 (other than the added test case), because the strict-time.patch has already been applied to the Go repository via CL 105235 in Go 1.11.1. Reference: https://groups.google.com/d/msg/golang-announce/pFXKAfoVJqw/eyDgSLVYAgAJ. Fixes golang/go#28036. Change-Id: Iacf9900a21c4b2f7bf5ac756be2cdbd8ac0be815 Reviewed-on: https://go-review.googlesource.com/c/140097 Reviewed-by: Brad Fitzpatrick --- Dockerfile | 4 +-- strict-time.patch | 83 ----------------------------------------------- 2 files changed, 1 insertion(+), 86 deletions(-) delete mode 100644 strict-time.patch diff --git a/Dockerfile b/Dockerfile index 6e3d1386..b02227fe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,12 +8,11 @@ ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap ENV CGO_ENABLED=0 -ENV GO_VERSION 1.10.3 +ENV GO_VERSION 1.11.1 ENV BUILD_DEPS 'curl bzip2 git gcc patch libc6-dev ca-certificates' # Fake time COPY enable-fake-time.patch /usr/local/playground/ -COPY strict-time.patch /usr/local/playground/ # Fake file system COPY fake_fs.lst /usr/local/playground/ @@ -30,7 +29,6 @@ RUN tar -C /usr/local/ -vxzf /tmp/go.tar.gz RUN cp -R /usr/local/go $GOROOT_BOOTSTRAP # Apply the fake time and fake filesystem patches. RUN patch /usr/local/go/src/runtime/rt0_nacl_amd64p32.s /usr/local/playground/enable-fake-time.patch -RUN patch -p1 -d /usr/local/go -Date: Fri, 6 Apr 2018 16:19:30 -0400 -Subject: [PATCH] runtime: make Playground timestamps change when the stream fd - changes - -The process reading the output of the binary may read stderr and -stdout separately, and may interleave reads from the two streams -arbitrarily. Because we explicitly serialize writes on the writer -side, we can reuse a timestamp within a single stream without losing -information; however, if we use the same timestamp for write on both -streams, the reader can't tell how to interleave them. - -This change ensures that every time we change between the two fds, we -also bump the timestamp. That way, writes within a stream continue to -show the same timestamp, but a sorted merge of the contents of the two -streams always interleaves them in the correct order. - -This still requires a corresponding change to the Playground parser to -actually reconstruct the correct interleaving. It currently merges the -two streams without reordering them; it should instead buffer them -separately and perform a sorted merge. (See -https://golang.org/cl/105496.) - -Updates golang/go#24615. -Updates golang/go#24659. - -Change-Id: Id789dfcc02eb4247906c9ddad38dac50cf829979 ---- - src/runtime/os_nacl.go | 9 +++++++++ - src/runtime/sys_nacl_amd64p32.s | 16 ++++++++++++++++ - 2 files changed, 25 insertions(+) - -diff --git a/src/runtime/os_nacl.go b/src/runtime/os_nacl.go -index d03cb8faf2..7b8a7d548e 100644 ---- a/src/runtime/os_nacl.go -+++ b/src/runtime/os_nacl.go -@@ -289,6 +289,15 @@ type gsignalStack struct{} - - var writelock uint32 // test-and-set spin lock for write - -+// lastfaketime stores the last faketime value written to fd 1 or 2. -+var lastfaketime int64 -+ -+// lastfaketimefd stores the fd to which lastfaketime was written. -+// -+// Subsequent writes to the same fd may use the same timestamp, -+// but the timestamp must increase if the fd changes. -+var lastfaketimefd int32 -+ - /* - An attempt at IRT. Doesn't work. See end of sys_nacl_amd64.s. - -diff --git a/src/runtime/sys_nacl_amd64p32.s b/src/runtime/sys_nacl_amd64p32.s -index ff4c2e7bb5..4c4d509576 100644 ---- a/src/runtime/sys_nacl_amd64p32.s -+++ b/src/runtime/sys_nacl_amd64p32.s -@@ -89,6 +89,22 @@ playback: - CMPL BX, $0 - JNE playback - -+ MOVQ runtime·lastfaketime(SB), CX -+ MOVL runtime·lastfaketimefd(SB), BX -+ CMPL DI, BX -+ JE samefd -+ -+ // If the current fd doesn't match the fd of the previous write, -+ // ensure that the timestamp is strictly greater. That way, we can -+ // recover the original order even if we read the fds separately. -+ INCQ CX -+ MOVL DI, runtime·lastfaketimefd(SB) -+ -+samefd: -+ CMPQ AX, CX -+ CMOVQLT CX, AX -+ MOVQ AX, runtime·lastfaketime(SB) -+ - // Playback header: 0 0 P B <8-byte time> <4-byte data length> - MOVL $(('B'<<24) | ('P'<<16)), 0(SP) - BSWAPQ AX --- -2.17.0.484.g0c8726318c-goog - From 8cbc41a23e6fa6d353bce527020ac5f11d670cdb Mon Sep 17 00:00:00 2001 From: Yury Smolsky Date: Mon, 8 Oct 2018 12:50:31 +0300 Subject: [PATCH 005/148] playground: mention vet in about text Fixes golang/go#25462 Change-Id: I11b861352b53f9492cd1f5c6d8c434edd68e4bd9 Reviewed-on: https://go-review.googlesource.com/c/140497 Reviewed-by: Andrew Bonventre --- edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edit.html b/edit.html index f3cd3be9..f783dd69 100644 --- a/edit.html +++ b/edit.html @@ -136,7 +136,7 @@

The Go Playground is a web service that runs on golang.org's servers. -The service receives a Go program, compiles, links, and +The service receives a Go program, vets, compiles, links, and runs the program inside a sandbox, then returns the output.

From e03e82975f78ba0f88b87976f5328eaf38485ec7 Mon Sep 17 00:00:00 2001 From: Yury Smolsky Date: Thu, 11 Oct 2018 12:51:49 +0300 Subject: [PATCH 006/148] playground: make outcome of "Run" clearer This CL changes the last line displayed after the program was run to display more detail on what happened. For more details see CL 141477. Dockerfile in playground repo needs to be updated to include x/tools with CL 141477. Updates golang/go#10590 Updates golang/go#25454 Change-Id: I6bdef66e70d693540e4e7d30478ae9f0bf85d1ba Reviewed-on: https://go-review.googlesource.com/c/141478 Reviewed-by: Andrew Bonventre Run-TryBot: Andrew Bonventre --- sandbox.go | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/sandbox.go b/sandbox.go index f63c360a..9b80f78d 100644 --- a/sandbox.go +++ b/sandbox.go @@ -28,6 +28,7 @@ import ( "reflect" "runtime" "strings" + "syscall" "text/template" "time" @@ -49,8 +50,11 @@ type request struct { } type response struct { - Errors string - Events []Event + Errors string + Events []Event + Status int + IsTest bool + TestsFailed int } // commandHandler returns an http.HandlerFunc. @@ -276,6 +280,8 @@ func main() { } `)) +var failedTestPattern = "--- FAIL" + // compileAndRun tries to build and run a user program. // The output of successfully ran program is returned in *response.Events. // If a program cannot be built or has timed out, @@ -334,19 +340,36 @@ func compileAndRun(req *request) (*response, error) { rec := new(Recorder) cmd.Stdout = rec.Stdout() cmd.Stderr = rec.Stderr() + var status int if err := cmd.Run(); err != nil { if ctx.Err() == context.DeadlineExceeded { - return &response{Errors: "process took too long"}, nil + // Send what was captured before the timeout. + events, err := rec.Events() + if err != nil { + return nil, fmt.Errorf("error decoding events: %v", err) + } + return &response{Errors: "process took too long", Events: events}, nil } - if _, ok := err.(*exec.ExitError); !ok { + exitErr, ok := err.(*exec.ExitError) + if !ok { return nil, fmt.Errorf("error running sandbox: %v", err) } + if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { + status = ws.ExitStatus() + } } events, err := rec.Events() if err != nil { return nil, fmt.Errorf("error decoding events: %v", err) } - return &response{Events: events}, nil + var fails int + if testParam != "" { + // In case of testing the TestsFailed field contains how many tests have failed. + for _, e := range events { + fails += strings.Count(e.Message, failedTestPattern) + } + } + return &response{Events: events, Status: status, IsTest: testParam != "", TestsFailed: fails}, nil } func (s *server) healthCheck() error { From ed387e609b15eb37126207f93fd65348ee270149 Mon Sep 17 00:00:00 2001 From: Yury Smolsky Date: Tue, 16 Oct 2018 22:32:25 +0300 Subject: [PATCH 007/148] Dockerfile: include updated x/tools containing CL 141477 Updates golang/go#10590 Updates golang/go#25454 Change-Id: I807608979062f57268bf2b80edbd37d88e74b8f7 Reviewed-on: https://go-review.googlesource.com/c/142637 Reviewed-by: Brad Fitzpatrick --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index b02227fe..878cf700 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,9 +70,9 @@ ENV REV=b7ef84aaf62aa3e70962625c80a571ae7c17cb40 RUN go get -d golang.org/x/text/secure/bidirule `#and 4 other pkgs` &&\ (cd /go/src/golang.org/x/text && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo golang.org/x/tools at 59602fd (2018-10-04) -ENV REV=59602fdee893255faef9b2cd3aa8f880ea85265c -RUN go get -d golang.org/x/tools/go/ast/astutil `#and 3 other pkgs` &&\ +# Repo golang.org/x/tools at 265a513 (2018-10-16) +ENV REV=265a51357114e0a09a88f3436f5eaef823bd046c +RUN go get -d golang.org/x/tools/go/ast/astutil `#and 5 other pkgs` &&\ (cd /go/src/golang.org/x/tools && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) # Repo google.golang.org/api at ab90adb (2018-02-22) From ad3cdb956e904d1db6e36e47a0ecd8f733023604 Mon Sep 17 00:00:00 2001 From: Yury Smolsky Date: Wed, 24 Oct 2018 00:21:10 +0300 Subject: [PATCH 008/148] playground: fix the test broken by CL 141478 New fields were added to the response struct in CL 141478. That was not accounted in the TestCommandHandler test. This CL fixes the test by adding required fields. Updates golang/go#25454 Change-Id: I374cef6199235d781bd83a60156ca4f0d90898ee Reviewed-on: https://go-review.googlesource.com/c/144078 Reviewed-by: Brad Fitzpatrick Reviewed-by: Yury Smolsky Reviewed-by: Katie Hockman --- server_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server_test.go b/server_test.go index 63141930..34c8c265 100644 --- a/server_test.go +++ b/server_test.go @@ -192,12 +192,12 @@ func TestCommandHandler(t *testing.T) { {"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil}, {"Standard flow", http.MethodPost, http.StatusOK, []byte(`{"Body":"ok"}`), - []byte(`{"Errors":"","Events":[{"Message":"ok","Kind":"stdout","Delay":0}]} + []byte(`{"Errors":"","Events":[{"Message":"ok","Kind":"stdout","Delay":0}],"Status":0,"IsTest":false,"TestsFailed":0} `), }, {"Errors in response", http.MethodPost, http.StatusOK, []byte(`{"Body":"error"}`), - []byte(`{"Errors":"errors","Events":null} + []byte(`{"Errors":"errors","Events":null,"Status":0,"IsTest":false,"TestsFailed":0} `), }, {"Out of memory error in response body event message", http.MethodPost, http.StatusInternalServerError, From cadbb20a62ad01569ee6c04707c5c92fb76db3ad Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Wed, 24 Oct 2018 14:10:53 -0400 Subject: [PATCH 009/148] Dockerfile: update golang.org/x/talks/... import path CL 141317 moved the talk content in the content/ subdirectory. This CL follows up on that change by updating an import path accordingly. It fixes the following build error: Step 52/61 : RUN GOOS=nacl GOARCH=amd64p32 go get [...] golang.org/x/talks/2016/applicative/google [...] [...]: cannot find package "golang.org/x/talks/2016/applicative/google" in any of: /usr/local/go/src/golang.org/x/talks/2016/applicative/google (from $GOROOT) /go/src/golang.org/x/talks/2016/applicative/google (from $GOPATH) The command [...] returned a non-zero code: 1 Change-Id: I0b711d32023066a8b4a16ceb4cbe2c7b79d66f77 Reviewed-on: https://go-review.googlesource.com/c/144477 Reviewed-by: Brad Fitzpatrick --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 878cf700..646866d1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -183,7 +183,7 @@ RUN GOOS=nacl GOARCH=amd64p32 go get \ golang.org/x/tour/reader \ golang.org/x/tour/tree \ golang.org/x/tour/wc \ - golang.org/x/talks/2016/applicative/google && \ + golang.org/x/talks/content/2016/applicative/google && \ rm -rf $GOPATH/src/golang.org/x/tour/.git && \ rm -rf $GOPATH/src/golang.org/x/talks/.git From d134d7d391d04bb6c1c7cd8abb1f3eec872c7de6 Mon Sep 17 00:00:00 2001 From: Katie Hockman Date: Tue, 23 Oct 2018 18:12:08 -0400 Subject: [PATCH 010/148] playground: prevent caching memory-related compile time errors It is important that nondeterministic errors are not cached. Otherwise, playground users may encounter errors from a prevous run when the current run may have otherwise succeeded. The existing implementation prevents runtime OOM errors from being cached. This change prevents compile time OOM errors from being cached as well. See related: https://golang.org/cl/130035 Fixes golang/go#28283 Change-Id: If734915935cc1013bcb9a87d0ae6ecd67505f231 Reviewed-on: https://go-review.googlesource.com/c/144297 Reviewed-by: Dmitri Shuralyov --- sandbox.go | 14 +++++++++++--- server_test.go | 10 ++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/sandbox.go b/sandbox.go index 9b80f78d..59880855 100644 --- a/sandbox.go +++ b/sandbox.go @@ -42,8 +42,9 @@ const ( progName = "prog.go" ) -// Runtime error strings that will not be cached if found in an Event message. -var nonCachingRuntimeErrors = []string{"out of memory", "cannot allocate memory"} +// Responses that contain these strings will not be cached due to +// their non-deterministic nature. +var nonCachingErrors = []string{"out of memory", "cannot allocate memory"} type request struct { Body string @@ -94,11 +95,18 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*res http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } + for _, e := range nonCachingErrors { + if strings.Contains(resp.Errors, e) { + s.log.Errorf("cmdFunc compilation error: %q", resp.Errors) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + } for _, el := range resp.Events { if el.Kind != "stderr" { continue } - for _, e := range nonCachingRuntimeErrors { + for _, e := range nonCachingErrors { if strings.Contains(el.Message, e) { s.log.Errorf("cmdFunc runtime error: %q", el.Message) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) diff --git a/server_test.go b/server_test.go index 34c8c265..4ff2f034 100644 --- a/server_test.go +++ b/server_test.go @@ -175,6 +175,12 @@ func TestCommandHandler(t *testing.T) { if r.Body == "allocate-memory-error" { return &response{Events: []Event{{"cannot allocate memory", "stderr", 0}}}, nil } + if r.Body == "oom-compile-error" { + return &response{Errors: "out of memory"}, nil + } + if r.Body == "allocate-memory-compile-error" { + return &response{Errors: "cannot allocate memory"}, nil + } resp := &response{Events: []Event{{r.Body, "stdout", 0}}} return resp, nil }) @@ -204,6 +210,10 @@ func TestCommandHandler(t *testing.T) { []byte(`{"Body":"oom-error"}`), nil}, {"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"allocate-memory-error"}`), nil}, + {"Out of memory error in response errors", http.MethodPost, http.StatusInternalServerError, + []byte(`{"Body":"oom-compile-error"}`), nil}, + {"Cannot allocate memory error in response errors", http.MethodPost, http.StatusInternalServerError, + []byte(`{"Body":"allocate-memory-compile-error"}`), nil}, } for _, tc := range testCases { From 22a239636ddbf367ea8e330959da51de9761c669 Mon Sep 17 00:00:00 2001 From: Yury Smolsky Date: Thu, 1 Nov 2018 12:58:06 +0200 Subject: [PATCH 011/148] playground: remove stale TODO about exit code This feature was implemented. Change-Id: I6b0d9b121f2ccf517bb0f7c7be7c2aa602456acc Reviewed-on: https://go-review.googlesource.com/c/146617 Reviewed-by: Brad Fitzpatrick --- sandbox.go | 1 - 1 file changed, 1 deletion(-) diff --git a/sandbox.go b/sandbox.go index 59880855..7fb69647 100644 --- a/sandbox.go +++ b/sandbox.go @@ -4,7 +4,6 @@ // TODO(andybons): add logging // TODO(andybons): restrict memory use -// TODO(andybons): send exit code to user package main From e08a6e0686921c5b54593cdb6b74709dc4c30a9f Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Tue, 13 Nov 2018 15:40:55 -0500 Subject: [PATCH 012/148] playground: update snippet to tell version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous snippet was created almost 4 years ago in CL 2308. Update to a more modern snippet (created today) for the following reasons: • Apply gofmt (previous one was missing a newline at EOF). • It uses the current ID generation scheme, making it possible to re-create the same snippet in another playground instance. Change-Id: I50157f9a8d313f3c826d4289c91c42049ac5bd0d Reviewed-on: https://go-review.googlesource.com/c/149341 Reviewed-by: Brad Fitzpatrick --- edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/edit.html b/edit.html index f783dd69..2b5ded86 100644 --- a/edit.html +++ b/edit.html @@ -180,7 +180,7 @@

The playground uses the latest stable release of Go.
-The current version is {{.GoVersion}}. +The current version is {{.GoVersion}}.

From 381a0796f10deb59bf8f2f917125145aa72fa893 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Tue, 26 Feb 2019 02:18:34 -0500 Subject: [PATCH 013/148] playground: use Go 1.12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Go 1.12 has been released¹ and should be used, so that it's possible to play with Go programs that rely on Go 1.12-only features. In Go 1.12, build cache is required. Either GOCACHE must be set, or HOME must be set so that GOCACHE can be implicitly deduced. Set HOME and pass it on to the go build invocation inside the compileAndRun function. This fixes the following error, detected by the playground test: 2019/02/26 06:28:44 compile error: build cache is required, but could not be located: GOCACHE is not defined and neither $XDG_CACHE_HOME nor $HOME are defined The command '/bin/sh -c /app/playground test' returned a non-zero code: 1 This is related to issues golang/go#29243 and golang/go#29251, and the fix in CL 154181. ¹ https://groups.google.com/d/msg/golang-announce/Y1RZqnCKUKY/N9yK5c8iBgAJ Fixes golang/go#30397 Change-Id: I44c5c8928060732f31fd935b114568258c7298c7 Reviewed-on: https://go-review.googlesource.com/c/163798 Reviewed-by: Brad Fitzpatrick --- Dockerfile | 6 +++++- sandbox.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 646866d1..30453d17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap ENV CGO_ENABLED=0 -ENV GO_VERSION 1.11.1 +ENV GO_VERSION 1.12 ENV BUILD_DEPS 'curl bzip2 git gcc patch libc6-dev ca-certificates' # Fake time @@ -174,6 +174,10 @@ RUN apt-get update && apt-get install -y git ca-certificates --no-install-recomm COPY --from=builder /usr/local/go /usr/local/go COPY --from=builder /tmp/sel_ldr_x86_64 /usr/local/bin +# For implicit GOCACHE (issues 29243 and 29251), set HOME: +RUN mkdir -p /home/gopher +ENV HOME /home/gopher + ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH diff --git a/sandbox.go b/sandbox.go index 7fb69647..8e21a967 100644 --- a/sandbox.go +++ b/sandbox.go @@ -324,7 +324,7 @@ func compileAndRun(req *request) (*response, error) { exe := filepath.Join(tmpDir, "a.out") cmd := exec.Command("go", "build", "-o", exe, in) - cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOPATH=" + os.Getenv("GOPATH")} + cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOPATH=" + os.Getenv("GOPATH"), "HOME=" + os.Getenv("HOME")} if out, err := cmd.CombinedOutput(); err != nil { if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. From 15cfee74c7842ed74cf66987c1f7ae2e290a2b76 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Tue, 26 Feb 2019 12:59:45 -0500 Subject: [PATCH 014/148] playground: update dependencies Start using newer versions of dependencies, to reduce tech debt and make future updates more incremental. This change was generated by running make update-deps on linux/amd64, using current versions of all dependencies. Change-Id: I8e76e010a36cd479aa491f722b4eb8ef9fdf89a0 Reviewed-on: https://go-review.googlesource.com/c/163799 Reviewed-by: Brad Fitzpatrick --- Dockerfile | 112 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/Dockerfile b/Dockerfile index 30453d17..c1962079 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,67 +35,82 @@ RUN cd /usr/local/go/src && GOOS=nacl GOARCH=amd64p32 ./make.bash --no-clean # BEGIN deps (run `make update-deps` to update) -# Repo cloud.google.com/go at 3afaae4 (2018-03-02) -ENV REV=3afaae429987a1884530d6018d6e963a932d28c0 +# Repo cloud.google.com/go at e2c125c (2019-02-22) +ENV REV=e2c125ceac8b663cfcf4610477d4d67827377cb7 RUN go get -d cloud.google.com/go/compute/metadata `#and 6 other pkgs` &&\ (cd /go/src/cloud.google.com/go && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo github.com/bradfitz/gomemcache at 1952afa (2017-02-08) -ENV REV=1952afaa557dc08e8e0d89eafab110fb501c1a2b +# Repo github.com/bradfitz/gomemcache at bc664df (2018-07-10) +ENV REV=bc664df9673713a0ccf26e3b55a673ec7301088b RUN go get -d github.com/bradfitz/gomemcache/memcache &&\ (cd /go/src/github.com/bradfitz/gomemcache && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo github.com/golang/protobuf at bbd03ef (2018-02-02) -ENV REV=bbd03ef6da3a115852eaf24c8a1c46aeb39aa175 +# Repo github.com/golang/protobuf at c823c79 (2019-02-05) +ENV REV=c823c79ea1570fb5ff454033735a8e68575d1d0f RUN go get -d github.com/golang/protobuf/proto `#and 8 other pkgs` &&\ (cd /go/src/github.com/golang/protobuf && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo github.com/googleapis/gax-go at 317e000 (2017-09-15) -ENV REV=317e0006254c44a0ac427cc52a0e083ff0b9622f -RUN go get -d github.com/googleapis/gax-go &&\ +# Repo github.com/googleapis/gax-go at ddfab93 (2019-01-11) +ENV REV=ddfab93c3faef4935403ac75a7c11f0e731dc181 +RUN go get -d github.com/googleapis/gax-go/v2 &&\ (cd /go/src/github.com/googleapis/gax-go && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo golang.org/x/net at ae89d30 (2018-03-11) -ENV REV=ae89d30ce0c63142b652837da33d782e2b0a9b25 +# Repo github.com/hashicorp/golang-lru at 20f1fb7 (2018-08-29) +ENV REV=20f1fb78b0740ba8c3cb143a61e86ba5c8669768 +RUN go get -d github.com/hashicorp/golang-lru/simplelru &&\ + (cd /go/src/github.com/hashicorp/golang-lru && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) + +# Repo go.opencensus.io at beafb2a (2019-02-22) +ENV REV=beafb2a85a579a4918ba259877a1625e9213a263 +RUN go get -d go.opencensus.io `#and 13 other pkgs` &&\ + (cd /go/src/go.opencensus.io && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) + +# Repo golang.org/x/net at fe579d4 (2019-02-25) +ENV REV=fe579d43d83210096a79b46dcca0e3721058393a RUN go get -d golang.org/x/net/context `#and 8 other pkgs` &&\ (cd /go/src/golang.org/x/net && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo golang.org/x/oauth2 at 2f32c3a (2018-02-28) -ENV REV=2f32c3ac0fa4fb807a0fcefb0b6f2468a0d99bd0 +# Repo golang.org/x/oauth2 at 9b3c759 (2019-02-20) +ENV REV=9b3c75971fc92dd27c6436a37c05c831498658f1 RUN go get -d golang.org/x/oauth2 `#and 5 other pkgs` &&\ (cd /go/src/golang.org/x/oauth2 && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo golang.org/x/text at b7ef84a (2018-03-02) -ENV REV=b7ef84aaf62aa3e70962625c80a571ae7c17cb40 +# Repo golang.org/x/sys at cc5685c (2019-02-25) +ENV REV=cc5685c2db1239775905f3911f0067c0fa74762f +RUN go get -d golang.org/x/sys/unix &&\ + (cd /go/src/golang.org/x/sys && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) + +# Repo golang.org/x/text at d14c52b (2019-02-25) +ENV REV=d14c52b222ee852cdba8b07206ca0c614b389876 RUN go get -d golang.org/x/text/secure/bidirule `#and 4 other pkgs` &&\ (cd /go/src/golang.org/x/text && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo golang.org/x/tools at 265a513 (2018-10-16) -ENV REV=265a51357114e0a09a88f3436f5eaef823bd046c -RUN go get -d golang.org/x/tools/go/ast/astutil `#and 5 other pkgs` &&\ +# Repo golang.org/x/tools at 2dc4ef2 (2019-02-25) +ENV REV=2dc4ef2775b8122dd5afe2c18fd6f775e87f89e5 +RUN go get -d golang.org/x/tools/go/ast/astutil `#and 12 other pkgs` &&\ (cd /go/src/golang.org/x/tools && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo google.golang.org/api at ab90adb (2018-02-22) -ENV REV=ab90adb3efa287b869ecb698db42f923cc734972 +# Repo google.golang.org/api at 8a550ba (2019-02-24) +ENV REV=8a550ba84cafabe9b2262c41303f31e5a4626318 RUN go get -d google.golang.org/api/googleapi `#and 6 other pkgs` &&\ (cd /go/src/google.golang.org/api && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo google.golang.org/genproto at 2c5e7ac (2018-03-02) -ENV REV=2c5e7ac708aaa719366570dd82bda44541ca2a63 -RUN go get -d google.golang.org/genproto/googleapis/api/annotations `#and 4 other pkgs` &&\ +# Repo google.golang.org/genproto at 082222b (2019-02-19) +ENV REV=082222b4a5c572e33e82ee9162d1352c7cf38682 +RUN go get -d google.golang.org/genproto/googleapis/api/annotations `#and 5 other pkgs` &&\ (cd /go/src/google.golang.org/genproto && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) -# Repo google.golang.org/grpc at f0a1202 (2018-02-28) -ENV REV=f0a1202acdc5c4702be05098d5ff8e9b3b444442 -RUN go get -d google.golang.org/grpc `#and 24 other pkgs` &&\ +# Repo google.golang.org/grpc at 40cb561 (2019-02-25) +ENV REV=40cb5618f475e7b9d61aa7920ae4b04ef9bbaf89 +RUN go get -d google.golang.org/grpc `#and 32 other pkgs` &&\ (cd /go/src/google.golang.org/grpc && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) # Optimization to speed up iterative development, not necessary for correctness: RUN go install cloud.google.com/go/compute/metadata \ cloud.google.com/go/datastore \ cloud.google.com/go/internal \ - cloud.google.com/go/internal/atomiccache \ cloud.google.com/go/internal/fields \ + cloud.google.com/go/internal/trace \ cloud.google.com/go/internal/version \ github.com/bradfitz/gomemcache/memcache \ github.com/golang/protobuf/proto \ @@ -106,27 +121,51 @@ RUN go install cloud.google.com/go/compute/metadata \ github.com/golang/protobuf/ptypes/struct \ github.com/golang/protobuf/ptypes/timestamp \ github.com/golang/protobuf/ptypes/wrappers \ - github.com/googleapis/gax-go \ + github.com/googleapis/gax-go/v2 \ + github.com/hashicorp/golang-lru/simplelru \ + go.opencensus.io \ + go.opencensus.io/exemplar \ + go.opencensus.io/internal \ + go.opencensus.io/internal/tagencoding \ + go.opencensus.io/plugin/ocgrpc \ + go.opencensus.io/stats \ + go.opencensus.io/stats/internal \ + go.opencensus.io/stats/view \ + go.opencensus.io/tag \ + go.opencensus.io/trace \ + go.opencensus.io/trace/internal \ + go.opencensus.io/trace/propagation \ + go.opencensus.io/trace/tracestate \ golang.org/x/net/context \ golang.org/x/net/context/ctxhttp \ + golang.org/x/net/http/httpguts \ golang.org/x/net/http2 \ golang.org/x/net/http2/hpack \ golang.org/x/net/idna \ golang.org/x/net/internal/timeseries \ - golang.org/x/net/lex/httplex \ golang.org/x/net/trace \ golang.org/x/oauth2 \ golang.org/x/oauth2/google \ golang.org/x/oauth2/internal \ golang.org/x/oauth2/jws \ golang.org/x/oauth2/jwt \ + golang.org/x/sys/unix \ golang.org/x/text/secure/bidirule \ golang.org/x/text/transform \ golang.org/x/text/unicode/bidi \ golang.org/x/text/unicode/norm \ golang.org/x/tools/go/ast/astutil \ + golang.org/x/tools/go/gcexportdata \ + golang.org/x/tools/go/internal/cgo \ + golang.org/x/tools/go/internal/gcimporter \ + golang.org/x/tools/go/internal/packagesdriver \ + golang.org/x/tools/go/packages \ golang.org/x/tools/godoc/static \ golang.org/x/tools/imports \ + golang.org/x/tools/internal/fastwalk \ + golang.org/x/tools/internal/gopathwalk \ + golang.org/x/tools/internal/module \ + golang.org/x/tools/internal/semver \ google.golang.org/api/googleapi \ google.golang.org/api/googleapi/internal/uritemplates \ google.golang.org/api/internal \ @@ -135,21 +174,31 @@ RUN go install cloud.google.com/go/compute/metadata \ google.golang.org/api/transport/grpc \ google.golang.org/genproto/googleapis/api/annotations \ google.golang.org/genproto/googleapis/datastore/v1 \ + google.golang.org/genproto/googleapis/rpc/code \ google.golang.org/genproto/googleapis/rpc/status \ google.golang.org/genproto/googleapis/type/latlng \ google.golang.org/grpc \ google.golang.org/grpc/balancer \ google.golang.org/grpc/balancer/base \ google.golang.org/grpc/balancer/roundrobin \ + google.golang.org/grpc/binarylog/grpc_binarylog_v1 \ google.golang.org/grpc/codes \ google.golang.org/grpc/connectivity \ google.golang.org/grpc/credentials \ + google.golang.org/grpc/credentials/internal \ google.golang.org/grpc/credentials/oauth \ google.golang.org/grpc/encoding \ google.golang.org/grpc/encoding/proto \ - google.golang.org/grpc/grpclb/grpc_lb_v1/messages \ google.golang.org/grpc/grpclog \ google.golang.org/grpc/internal \ + google.golang.org/grpc/internal/backoff \ + google.golang.org/grpc/internal/binarylog \ + google.golang.org/grpc/internal/channelz \ + google.golang.org/grpc/internal/envconfig \ + google.golang.org/grpc/internal/grpcrand \ + google.golang.org/grpc/internal/grpcsync \ + google.golang.org/grpc/internal/syscall \ + google.golang.org/grpc/internal/transport \ google.golang.org/grpc/keepalive \ google.golang.org/grpc/metadata \ google.golang.org/grpc/naming \ @@ -159,8 +208,7 @@ RUN go install cloud.google.com/go/compute/metadata \ google.golang.org/grpc/resolver/passthrough \ google.golang.org/grpc/stats \ google.golang.org/grpc/status \ - google.golang.org/grpc/tap \ - google.golang.org/grpc/transport + google.golang.org/grpc/tap # END deps # Add and compile playground daemon From be0fe039fea243c2bbdf4b04201e0cc182f36ea9 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 28 Feb 2019 18:03:52 +0000 Subject: [PATCH 015/148] playground: put GOCACHE in temp dir Fixes golang/go#30473 Change-Id: Iad5b60b98c12add932fa3be9174fe7ca4dcbb6b2 Reviewed-on: https://go-review.googlesource.com/c/164519 Reviewed-by: Dmitri Shuralyov --- Dockerfile | 4 ---- sandbox.go | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index c1962079..c6390fc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -222,10 +222,6 @@ RUN apt-get update && apt-get install -y git ca-certificates --no-install-recomm COPY --from=builder /usr/local/go /usr/local/go COPY --from=builder /tmp/sel_ldr_x86_64 /usr/local/bin -# For implicit GOCACHE (issues 29243 and 29251), set HOME: -RUN mkdir -p /home/gopher -ENV HOME /home/gopher - ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH diff --git a/sandbox.go b/sandbox.go index 8e21a967..7493213b 100644 --- a/sandbox.go +++ b/sandbox.go @@ -323,8 +323,9 @@ func compileAndRun(req *request) (*response, error) { } exe := filepath.Join(tmpDir, "a.out") + goCache := filepath.Join(tmpDir, "gocache") cmd := exec.Command("go", "build", "-o", exe, in) - cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOPATH=" + os.Getenv("GOPATH"), "HOME=" + os.Getenv("HOME")} + cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOPATH=" + os.Getenv("GOPATH"), "GOCACHE=" + goCache} if out, err := cmd.CombinedOutput(); err != nil { if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. From b884d8570d1d6b298baccf5066986b75cd849f24 Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Thu, 28 Feb 2019 13:47:27 -0500 Subject: [PATCH 016/148] README: document the need to increase cloud build timeout It's unfortunate to introduce an extra step of setting a global configuration value to the deploy process. It would be better if this could be an application-scoped setting specified in the app.yaml configuration. However, that doesn't seem to be possible, and after reading through documentation and waiting to hear any suggestions at golang/go#28046, I have not become aware of a better solution. Document this approach for now, and if we learn of a better solution in the future, this can be improved then. Fixes golang/go#28046 Change-Id: I2e9b7bbd38aa8313ff6eab0bf99dd2fe48f57544 Reviewed-on: https://go-review.googlesource.com/c/playground/+/164577 Reviewed-by: Andrew Bonventre Reviewed-by: Brad Fitzpatrick --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 63b05fa2..e900ea65 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,14 @@ cat /path/to/code.go | go run client.go | curl -s --upload-file - localhost:8080 # Deployment +Building the playground Docker container takes more than the default 10 minute time limit of cloud build, so increase its timeout first (note, `app/cloud_build_timeout` is a global configuration value): + +``` +gcloud config set app/cloud_build_timeout 1200 # 20 mins +``` + +Then: + ``` gcloud --project=golang-org --account=person@example.com app deploy app.yaml ``` From 33ccd9b32303a177e74464152f26b6a2704757d8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 9 May 2019 19:25:13 +0000 Subject: [PATCH 017/148] Dockerfile: update from jessie to stretch jessie is old (even by Debian standards) and doesn't even build anymore without changes, as the apt-get contents have moved to an archive URL. Debian stable is stretch these days, so upgrade to that. (Debian buster is almost the next stable, but not quite yet) Change-Id: I633edc56d92cea37c7ed01f6e85b4bea4ff3b412 Reviewed-on: https://go-review.googlesource.com/c/playground/+/176277 Reviewed-by: Dmitri Shuralyov --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index c6390fc3..81939546 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # Copyright 2017 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. -FROM debian:jessie as builder +FROM debian:stretch as builder LABEL maintainer "golang-dev@googlegroups.com" ENV GOPATH /go @@ -215,7 +215,7 @@ RUN go install cloud.google.com/go/compute/metadata \ COPY . /go/src/playground/ RUN go install playground -FROM debian:jessie +FROM debian:stretch RUN apt-get update && apt-get install -y git ca-certificates --no-install-recommends From 046e86367174dcb1c04c9492aa22912eb9c56813 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 9 May 2019 20:48:49 +0000 Subject: [PATCH 018/148] playground: support third-party imports (off by default for now) Also, modernize the Dockerfile while I'm at it, using multi-stage builds more aggressively, and stop using gitlock (golang/go#26872) and switch to using Go modules to build the playground. (This also turns the playground into a module.) Updates golang/go#31944 Change-Id: Ic6f6152469f1930fd04180a3d1e63ed92ea2cfbd Reviewed-on: https://go-review.googlesource.com/c/playground/+/176317 Reviewed-by: Yury Smolsky --- Dockerfile | 213 +++++++---------------------------------------------- Makefile | 4 - go.mod | 9 +++ go.sum | 67 +++++++++++++++++ sandbox.go | 46 +++++++++++- 5 files changed, 148 insertions(+), 191 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/Dockerfile b/Dockerfile index 81939546..ea9bfea9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,21 @@ # Copyright 2017 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. -FROM debian:stretch as builder + +FROM debian:stretch AS nacl + +RUN apt-get update && apt-get install -y --no-install-recommends curl bzip2 ca-certificates + +RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 + +FROM debian:stretch AS build LABEL maintainer "golang-dev@googlegroups.com" ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap -ENV CGO_ENABLED=0 -ENV GO_VERSION 1.12 -ENV BUILD_DEPS 'curl bzip2 git gcc patch libc6-dev ca-certificates' +ENV GO_VERSION 1.12.5 +ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' # Fake time COPY enable-fake-time.patch /usr/local/playground/ @@ -18,8 +24,6 @@ COPY fake_fs.lst /usr/local/playground/ RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends -RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 - # Get the Go binary. RUN curl -sSL https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz -o /tmp/go.tar.gz RUN curl -sSL https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz.sha256 -o /tmp/go.tar.gz.sha256 @@ -33,194 +37,28 @@ RUN cd /usr/local/go && go run misc/nacl/mkzip.go -p syscall /usr/local/playgrou # Re-build the Go toolchain. RUN cd /usr/local/go/src && GOOS=nacl GOARCH=amd64p32 ./make.bash --no-clean -# BEGIN deps (run `make update-deps` to update) - -# Repo cloud.google.com/go at e2c125c (2019-02-22) -ENV REV=e2c125ceac8b663cfcf4610477d4d67827377cb7 -RUN go get -d cloud.google.com/go/compute/metadata `#and 6 other pkgs` &&\ - (cd /go/src/cloud.google.com/go && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo github.com/bradfitz/gomemcache at bc664df (2018-07-10) -ENV REV=bc664df9673713a0ccf26e3b55a673ec7301088b -RUN go get -d github.com/bradfitz/gomemcache/memcache &&\ - (cd /go/src/github.com/bradfitz/gomemcache && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo github.com/golang/protobuf at c823c79 (2019-02-05) -ENV REV=c823c79ea1570fb5ff454033735a8e68575d1d0f -RUN go get -d github.com/golang/protobuf/proto `#and 8 other pkgs` &&\ - (cd /go/src/github.com/golang/protobuf && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo github.com/googleapis/gax-go at ddfab93 (2019-01-11) -ENV REV=ddfab93c3faef4935403ac75a7c11f0e731dc181 -RUN go get -d github.com/googleapis/gax-go/v2 &&\ - (cd /go/src/github.com/googleapis/gax-go && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) +RUN mkdir /gocache +ENV GOCACHE /gocache +ENV GO111MODULE on -# Repo github.com/hashicorp/golang-lru at 20f1fb7 (2018-08-29) -ENV REV=20f1fb78b0740ba8c3cb143a61e86ba5c8669768 -RUN go get -d github.com/hashicorp/golang-lru/simplelru &&\ - (cd /go/src/github.com/hashicorp/golang-lru && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo go.opencensus.io at beafb2a (2019-02-22) -ENV REV=beafb2a85a579a4918ba259877a1625e9213a263 -RUN go get -d go.opencensus.io `#and 13 other pkgs` &&\ - (cd /go/src/go.opencensus.io && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo golang.org/x/net at fe579d4 (2019-02-25) -ENV REV=fe579d43d83210096a79b46dcca0e3721058393a -RUN go get -d golang.org/x/net/context `#and 8 other pkgs` &&\ - (cd /go/src/golang.org/x/net && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo golang.org/x/oauth2 at 9b3c759 (2019-02-20) -ENV REV=9b3c75971fc92dd27c6436a37c05c831498658f1 -RUN go get -d golang.org/x/oauth2 `#and 5 other pkgs` &&\ - (cd /go/src/golang.org/x/oauth2 && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo golang.org/x/sys at cc5685c (2019-02-25) -ENV REV=cc5685c2db1239775905f3911f0067c0fa74762f -RUN go get -d golang.org/x/sys/unix &&\ - (cd /go/src/golang.org/x/sys && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo golang.org/x/text at d14c52b (2019-02-25) -ENV REV=d14c52b222ee852cdba8b07206ca0c614b389876 -RUN go get -d golang.org/x/text/secure/bidirule `#and 4 other pkgs` &&\ - (cd /go/src/golang.org/x/text && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo golang.org/x/tools at 2dc4ef2 (2019-02-25) -ENV REV=2dc4ef2775b8122dd5afe2c18fd6f775e87f89e5 -RUN go get -d golang.org/x/tools/go/ast/astutil `#and 12 other pkgs` &&\ - (cd /go/src/golang.org/x/tools && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo google.golang.org/api at 8a550ba (2019-02-24) -ENV REV=8a550ba84cafabe9b2262c41303f31e5a4626318 -RUN go get -d google.golang.org/api/googleapi `#and 6 other pkgs` &&\ - (cd /go/src/google.golang.org/api && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo google.golang.org/genproto at 082222b (2019-02-19) -ENV REV=082222b4a5c572e33e82ee9162d1352c7cf38682 -RUN go get -d google.golang.org/genproto/googleapis/api/annotations `#and 5 other pkgs` &&\ - (cd /go/src/google.golang.org/genproto && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo google.golang.org/grpc at 40cb561 (2019-02-25) -ENV REV=40cb5618f475e7b9d61aa7920ae4b04ef9bbaf89 -RUN go get -d google.golang.org/grpc `#and 32 other pkgs` &&\ - (cd /go/src/google.golang.org/grpc && (git cat-file -t $REV 2>/dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Optimization to speed up iterative development, not necessary for correctness: -RUN go install cloud.google.com/go/compute/metadata \ - cloud.google.com/go/datastore \ - cloud.google.com/go/internal \ - cloud.google.com/go/internal/fields \ - cloud.google.com/go/internal/trace \ - cloud.google.com/go/internal/version \ - github.com/bradfitz/gomemcache/memcache \ - github.com/golang/protobuf/proto \ - github.com/golang/protobuf/protoc-gen-go/descriptor \ - github.com/golang/protobuf/ptypes \ - github.com/golang/protobuf/ptypes/any \ - github.com/golang/protobuf/ptypes/duration \ - github.com/golang/protobuf/ptypes/struct \ - github.com/golang/protobuf/ptypes/timestamp \ - github.com/golang/protobuf/ptypes/wrappers \ - github.com/googleapis/gax-go/v2 \ - github.com/hashicorp/golang-lru/simplelru \ - go.opencensus.io \ - go.opencensus.io/exemplar \ - go.opencensus.io/internal \ - go.opencensus.io/internal/tagencoding \ - go.opencensus.io/plugin/ocgrpc \ - go.opencensus.io/stats \ - go.opencensus.io/stats/internal \ - go.opencensus.io/stats/view \ - go.opencensus.io/tag \ - go.opencensus.io/trace \ - go.opencensus.io/trace/internal \ - go.opencensus.io/trace/propagation \ - go.opencensus.io/trace/tracestate \ - golang.org/x/net/context \ - golang.org/x/net/context/ctxhttp \ - golang.org/x/net/http/httpguts \ - golang.org/x/net/http2 \ - golang.org/x/net/http2/hpack \ - golang.org/x/net/idna \ - golang.org/x/net/internal/timeseries \ - golang.org/x/net/trace \ - golang.org/x/oauth2 \ - golang.org/x/oauth2/google \ - golang.org/x/oauth2/internal \ - golang.org/x/oauth2/jws \ - golang.org/x/oauth2/jwt \ - golang.org/x/sys/unix \ - golang.org/x/text/secure/bidirule \ - golang.org/x/text/transform \ - golang.org/x/text/unicode/bidi \ - golang.org/x/text/unicode/norm \ - golang.org/x/tools/go/ast/astutil \ - golang.org/x/tools/go/gcexportdata \ - golang.org/x/tools/go/internal/cgo \ - golang.org/x/tools/go/internal/gcimporter \ - golang.org/x/tools/go/internal/packagesdriver \ - golang.org/x/tools/go/packages \ - golang.org/x/tools/godoc/static \ - golang.org/x/tools/imports \ - golang.org/x/tools/internal/fastwalk \ - golang.org/x/tools/internal/gopathwalk \ - golang.org/x/tools/internal/module \ - golang.org/x/tools/internal/semver \ - google.golang.org/api/googleapi \ - google.golang.org/api/googleapi/internal/uritemplates \ - google.golang.org/api/internal \ - google.golang.org/api/iterator \ - google.golang.org/api/option \ - google.golang.org/api/transport/grpc \ - google.golang.org/genproto/googleapis/api/annotations \ - google.golang.org/genproto/googleapis/datastore/v1 \ - google.golang.org/genproto/googleapis/rpc/code \ - google.golang.org/genproto/googleapis/rpc/status \ - google.golang.org/genproto/googleapis/type/latlng \ - google.golang.org/grpc \ - google.golang.org/grpc/balancer \ - google.golang.org/grpc/balancer/base \ - google.golang.org/grpc/balancer/roundrobin \ - google.golang.org/grpc/binarylog/grpc_binarylog_v1 \ - google.golang.org/grpc/codes \ - google.golang.org/grpc/connectivity \ - google.golang.org/grpc/credentials \ - google.golang.org/grpc/credentials/internal \ - google.golang.org/grpc/credentials/oauth \ - google.golang.org/grpc/encoding \ - google.golang.org/grpc/encoding/proto \ - google.golang.org/grpc/grpclog \ - google.golang.org/grpc/internal \ - google.golang.org/grpc/internal/backoff \ - google.golang.org/grpc/internal/binarylog \ - google.golang.org/grpc/internal/channelz \ - google.golang.org/grpc/internal/envconfig \ - google.golang.org/grpc/internal/grpcrand \ - google.golang.org/grpc/internal/grpcsync \ - google.golang.org/grpc/internal/syscall \ - google.golang.org/grpc/internal/transport \ - google.golang.org/grpc/keepalive \ - google.golang.org/grpc/metadata \ - google.golang.org/grpc/naming \ - google.golang.org/grpc/peer \ - google.golang.org/grpc/resolver \ - google.golang.org/grpc/resolver/dns \ - google.golang.org/grpc/resolver/passthrough \ - google.golang.org/grpc/stats \ - google.golang.org/grpc/status \ - google.golang.org/grpc/tap -# END deps +# Pre-build some packages to speed final install later. +RUN go install cloud.google.com/go/compute/metadata +RUN go install cloud.google.com/go/datastore +RUN go install github.com/bradfitz/gomemcache/memcache +RUN go install golang.org/x/tools/godoc/static +RUN go install golang.org/x/tools/imports # Add and compile playground daemon COPY . /go/src/playground/ -RUN go install playground +WORKDIR /go/src/playground +RUN go install FROM debian:stretch RUN apt-get update && apt-get install -y git ca-certificates --no-install-recommends -COPY --from=builder /usr/local/go /usr/local/go -COPY --from=builder /tmp/sel_ldr_x86_64 /usr/local/bin +COPY --from=build /usr/local/go /usr/local/go +COPY --from=nacl /tmp/sel_ldr_x86_64 /usr/local/bin ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH @@ -247,7 +85,7 @@ RUN mkdir -p $GOPATH/src/code.google.com/p/go-tour && \ RUN mkdir /app -COPY --from=builder /go/bin/playground /app +COPY --from=build /go/bin/playground /app COPY edit.html /app COPY static /app/static WORKDIR /app @@ -255,5 +93,8 @@ WORKDIR /app # Run tests RUN /app/playground test +# Whether we allow third-party imports via proxy.golang.org: +ENV ALLOW_PLAY_MODULE_DOWNLOADS false + EXPOSE 8080 ENTRYPOINT ["/app/playground"] diff --git a/Makefile b/Makefile index e4c4d879..3fbb2ed4 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,5 @@ .PHONY: update-deps docker test -update-deps: - go install golang.org/x/build/cmd/gitlock - gitlock --update=Dockerfile golang.org/x/playground - docker: Dockerfile docker build -t playground . diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..522d24a7 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module golang.org/x/playground + +go 1.12 + +require ( + cloud.google.com/go v0.38.0 + github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 + golang.org/x/tools v0.0.0-20190509153222-73554e0f7805 +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..9bf92954 --- /dev/null +++ b/go.sum @@ -0,0 +1,67 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA= +github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190509153222-73554e0f7805 h1:1ufBXAsTpUhSmmPXEEs5PrGQSfnBhsjAd2SmVhp9xrY= +golang.org/x/tools v0.0.0-20190509153222-73554e0f7805/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sandbox.go b/sandbox.go index 7493213b..56ab9e3d 100644 --- a/sandbox.go +++ b/sandbox.go @@ -26,6 +26,7 @@ import ( "path/filepath" "reflect" "runtime" + "strconv" "strings" "syscall" "text/template" @@ -325,7 +326,23 @@ func compileAndRun(req *request) (*response, error) { exe := filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") cmd := exec.Command("go", "build", "-o", exe, in) - cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOPATH=" + os.Getenv("GOPATH"), "GOCACHE=" + goCache} + cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache} + if allowModuleDownloads(src) { + // Create a GOPATH just for modules to be downloaded + // into GOPATH/pkg/mod. + gopath, err := ioutil.TempDir("", "gopath") + if err != nil { + return nil, fmt.Errorf("error creating temp directory: %v", err) + } + defer os.RemoveAll(gopath) + cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY=https://proxy.golang.org", "GOPATH="+gopath) + } else { + + cmd.Env = append(cmd.Env, + "GO111MODULE=off", // in case it becomes on by default later + "GOPATH="+os.Getenv("GOPATH"), // contains old code.google.com/p/go-tour, etc + ) + } if out, err := cmd.CombinedOutput(); err != nil { if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. @@ -380,6 +397,21 @@ func compileAndRun(req *request) (*response, error) { return &response{Events: events, Status: status, IsTest: testParam != "", TestsFailed: fails}, nil } +// allowModuleDownloads reports whether the code snippet in src should be allowed +// to download modules. +func allowModuleDownloads(src []byte) bool { + if bytes.Contains(src, []byte(`"code.google.com/p/go-tour/`)) { + // This domain doesn't exist anymore but we want old snippets using + // these packages to still run, so the Dockerfile adds these packages + // at this name in $GOPATH. Any snippets using this old name wouldn't + // have expected (or been able to use) third-party packages anyway, + // so disabling modules and proxy.golang.org fetches is acceptable. + return false + } + v, _ := strconv.ParseBool(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) + return v +} + func (s *server) healthCheck() error { resp, err := compileAndRun(&request{Body: healthProg}) if err != nil { @@ -406,6 +438,11 @@ func (s *server) test() { if err := s.healthCheck(); err != nil { stdlog.Fatal(err) } + + // Enable module downloads for testing: + defer func(old string) { os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", old) }(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) + os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", "true") + for _, t := range tests { resp, err := compileAndRun(&request{Body: t.prog}) if err != nil { @@ -733,4 +770,11 @@ func main() { {"B\n", "stderr", time.Second - 2*time.Nanosecond}, {"A\n", "stdout", time.Second}, }}, + + // Test third-party imports: + {prog: ` +package main +import ("fmt"; "github.com/bradfitz/iter") +func main() { for i := range iter.N(5) { fmt.Println(i) } } +`, want: "0\n1\n2\n3\n4\n"}, } From 1cc919a72964db4e1eda88259a83004bfd1ddfad Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 10 May 2019 18:19:28 +0000 Subject: [PATCH 019/148] playground: let clients request vet check in same HTTP request as compile+run Also, move the tests to their own file and extend them a bit, give them names, and make the test step more verbose (only visible in docker build anyway). They were hogging up the sandbox file. Updates golang/go#31970 Change-Id: Id710ea613c77a5b16cc5e79545c0812d0f4650e3 Reviewed-on: https://go-review.googlesource.com/c/playground/+/176598 Reviewed-by: Andrew Bonventre Reviewed-by: Yury Smolsky --- README.md | 11 +- go.mod | 3 +- go.sum | 10 +- sandbox.go | 404 +++++------------------------------------------- tests.go | 444 +++++++++++++++++++++++++++++++++++++++++++++++++++++ vet.go | 38 ++++- 6 files changed, 537 insertions(+), 373 deletions(-) create mode 100644 tests.go diff --git a/README.md b/README.md index e900ea65..33bdcfc1 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,19 @@ Building the playground Docker container takes more than the default 10 minute t gcloud config set app/cloud_build_timeout 1200 # 20 mins ``` +Alternatively, to avoid Cloud Build and build locally: + +``` +make docker +docker tag playground:latest gcr.io/golang-org/playground:latest +docker push gcr.io/golang-org/playground:latest +gcloud --project=golang-org --account=you@google.com app deploy app.yaml --image-url=gcr.io/golang-org/playground:latest +``` + Then: ``` -gcloud --project=golang-org --account=person@example.com app deploy app.yaml +gcloud --project=golang-org --account=you@google.com app deploy app.yaml ``` # Contributing diff --git a/go.mod b/go.mod index 522d24a7..f68be743 100644 --- a/go.mod +++ b/go.mod @@ -5,5 +5,6 @@ go 1.12 require ( cloud.google.com/go v0.38.0 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 - golang.org/x/tools v0.0.0-20190509153222-73554e0f7805 + golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect + golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 ) diff --git a/go.sum b/go.sum index 9bf92954..8e9d91d0 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,14 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -33,6 +35,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -40,6 +44,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= @@ -51,11 +56,12 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190509153222-73554e0f7805 h1:1ufBXAsTpUhSmmPXEEs5PrGQSfnBhsjAd2SmVhp9xrY= -golang.org/x/tools v0.0.0-20190509153222-73554e0f7805/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 h1:zHwPzzQF2U6W4cSM2929cb7MvpB6dLYu9dHwYjOv+ag= +golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/sandbox.go b/sandbox.go index 56ab9e3d..56e96136 100644 --- a/sandbox.go +++ b/sandbox.go @@ -19,12 +19,10 @@ import ( "go/token" "io" "io/ioutil" - stdlog "log" "net/http" "os" "os/exec" "path/filepath" - "reflect" "runtime" "strconv" "strings" @@ -38,7 +36,8 @@ import ( const ( maxRunTime = 2 * time.Second - // progName is the program name in compiler errors + // progName is the implicit program name written to the temp + // dir and used in compiler and vet errors. progName = "prog.go" ) @@ -47,7 +46,8 @@ const ( var nonCachingErrors = []string{"out of memory", "cannot allocate memory"} type request struct { - Body string + Body string + WithVet bool // whether client supports vet response in a /compile request (Issue 31970) } type response struct { @@ -56,6 +56,14 @@ type response struct { Status int IsTest bool TestsFailed int + + // VetErrors, if non-empty, contains any vet errors. It is + // only populated if request.WithVet was true. + VetErrors string `json:",omitempty"` + // VetOK reports whether vet ran & passsed. It is only + // populated if request.WithVet was true. Only one of + // VetErrors or VetOK can be non-zero. + VetOK bool `json:",omitempty"` } // commandHandler returns an http.HandlerFunc. @@ -77,6 +85,7 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*res // are updated to always send JSON, this check is in place. if b := r.FormValue("body"); b != "" { req.Body = b + req.WithVet, _ = strconv.ParseBool(r.FormValue("withVet")) } else if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.log.Errorf("error decoding request: %v", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) @@ -182,7 +191,7 @@ func isTest(name, prefix string) bool { func getTestProg(src []byte) []byte { fset := token.NewFileSet() // Early bail for most cases. - f, err := parser.ParseFile(fset, "main.go", src, parser.ImportsOnly) + f, err := parser.ParseFile(fset, progName, src, parser.ImportsOnly) if err != nil || f.Name.Name != "main" { return nil } @@ -199,7 +208,7 @@ func getTestProg(src []byte) []byte { } // Parse everything and extract test names. - f, err = parser.ParseFile(fset, "main.go", src, parser.ParseComments) + f, err = parser.ParseFile(fset, progName, src, parser.ParseComments) if err != nil { return nil } @@ -303,7 +312,7 @@ func compileAndRun(req *request) (*response, error) { defer os.RemoveAll(tmpDir) src := []byte(req.Body) - in := filepath.Join(tmpDir, "main.go") + in := filepath.Join(tmpDir, progName) if err := ioutil.WriteFile(in, src, 0400); err != nil { return nil, fmt.Errorf("error creating temp file %q: %v", in, err) } @@ -326,29 +335,29 @@ func compileAndRun(req *request) (*response, error) { exe := filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") cmd := exec.Command("go", "build", "-o", exe, in) + var goPath string cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache} - if allowModuleDownloads(src) { + useModules := allowModuleDownloads(src) + if useModules { // Create a GOPATH just for modules to be downloaded // into GOPATH/pkg/mod. - gopath, err := ioutil.TempDir("", "gopath") + goPath, err = ioutil.TempDir("", "gopath") if err != nil { return nil, fmt.Errorf("error creating temp directory: %v", err) } - defer os.RemoveAll(gopath) - cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY=https://proxy.golang.org", "GOPATH="+gopath) + defer os.RemoveAll(goPath) + cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY=https://proxy.golang.org") } else { - - cmd.Env = append(cmd.Env, - "GO111MODULE=off", // in case it becomes on by default later - "GOPATH="+os.Getenv("GOPATH"), // contains old code.google.com/p/go-tour, etc - ) + goPath = os.Getenv("GOPATH") // contains old code.google.com/p/go-tour, etc + cmd.Env = append(cmd.Env, "GO111MODULE=off") // in case it becomes on by default later } + cmd.Env = append(cmd.Env, "GOPATH="+goPath) if out, err := cmd.CombinedOutput(); err != nil { if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. // Rewrite compiler errors to refer to progName - // instead of '/tmp/sandbox1234/main.go'. + // instead of '/tmp/sandbox1234/prog.go'. errs := strings.Replace(string(out), in, progName, -1) // "go build", invoked with a file name, puts this odd @@ -394,7 +403,21 @@ func compileAndRun(req *request) (*response, error) { fails += strings.Count(e.Message, failedTestPattern) } } - return &response{Events: events, Status: status, IsTest: testParam != "", TestsFailed: fails}, nil + var vetOut string + if req.WithVet { + vetOut, err = vetCheckInDir(tmpDir, goPath, useModules) + if err != nil { + return nil, fmt.Errorf("running vet: %v", err) + } + } + return &response{ + Events: events, + Status: status, + IsTest: testParam != "", + TestsFailed: fails, + VetErrors: vetOut, + VetOK: req.WithVet && vetOut == "", + }, nil } // allowModuleDownloads reports whether the code snippet in src should be allowed @@ -433,348 +456,3 @@ import "fmt" func main() { fmt.Print("ok") } ` - -func (s *server) test() { - if err := s.healthCheck(); err != nil { - stdlog.Fatal(err) - } - - // Enable module downloads for testing: - defer func(old string) { os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", old) }(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) - os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", "true") - - for _, t := range tests { - resp, err := compileAndRun(&request{Body: t.prog}) - if err != nil { - stdlog.Fatal(err) - } - if t.wantEvents != nil { - if !reflect.DeepEqual(resp.Events, t.wantEvents) { - stdlog.Fatalf("resp.Events = %q, want %q", resp.Events, t.wantEvents) - } - continue - } - if t.errors != "" { - if resp.Errors != t.errors { - stdlog.Fatalf("resp.Errors = %q, want %q", resp.Errors, t.errors) - } - continue - } - if resp.Errors != "" { - stdlog.Fatal(resp.Errors) - } - if len(resp.Events) == 0 { - stdlog.Fatalf("unexpected output: %q, want %q", "", t.want) - } - var b strings.Builder - for _, e := range resp.Events { - b.WriteString(e.Message) - } - if !strings.Contains(b.String(), t.want) { - stdlog.Fatalf("unexpected output: %q, want %q", b.String(), t.want) - } - } - fmt.Println("OK") -} - -var tests = []struct { - prog, want, errors string - wantEvents []Event -}{ - {prog: ` -package main - -import "time" - -func main() { - loc, err := time.LoadLocation("America/New_York") - if err != nil { - panic(err.Error()) - } - println(loc.String()) -} -`, want: "America/New_York"}, - - {prog: ` -package main - -import ( - "fmt" - "time" -) - -func main() { - fmt.Println(time.Now()) -} -`, want: "2009-11-10 23:00:00 +0000 UTC"}, - - {prog: ` -package main - -import ( - "fmt" - "time" -) - -func main() { - t1 := time.Tick(time.Second * 3) - t2 := time.Tick(time.Second * 7) - t3 := time.Tick(time.Second * 11) - end := time.After(time.Second * 19) - want := "112131211" - var got []byte - for { - var c byte - select { - case <-t1: - c = '1' - case <-t2: - c = '2' - case <-t3: - c = '3' - case <-end: - if g := string(got); g != want { - fmt.Printf("got %q, want %q\n", g, want) - } else { - fmt.Println("timers fired as expected") - } - return - } - got = append(got, c) - } -} -`, want: "timers fired as expected"}, - - {prog: ` -package main - -import ( - "code.google.com/p/go-tour/pic" - "code.google.com/p/go-tour/reader" - "code.google.com/p/go-tour/tree" - "code.google.com/p/go-tour/wc" -) - -var ( - _ = pic.Show - _ = reader.Validate - _ = tree.New - _ = wc.Test -) - -func main() { - println("ok") -} -`, want: "ok"}, - {prog: ` -package test - -func main() { - println("test") -} -`, want: "", errors: "package name must be main"}, - {prog: ` -package main - -import ( - "fmt" - "os" - "path/filepath" -) - -func main() { - filepath.Walk("/", func(path string, info os.FileInfo, err error) error { - fmt.Println(path) - return nil - }) -} -`, want: `/ -/dev -/dev/null -/dev/random -/dev/urandom -/dev/zero -/etc -/etc/group -/etc/hosts -/etc/passwd -/etc/resolv.conf -/tmp -/usr -/usr/local -/usr/local/go -/usr/local/go/lib -/usr/local/go/lib/time -/usr/local/go/lib/time/zoneinfo.zip`}, - {prog: ` -package main - -import "testing" - -func TestSanity(t *testing.T) { - if 1+1 != 2 { - t.Error("uhh...") - } -} -`, want: `=== RUN TestSanity ---- PASS: TestSanity (0.00s) -PASS`}, - - {prog: ` -package main - -func TestSanity(t *testing.T) { - t.Error("uhh...") -} - -func ExampleNotExecuted() { - // Output: it should not run -} -`, want: "", errors: "prog.go:4:20: undefined: testing\n"}, - - {prog: ` -package main - -import ( - "fmt" - "testing" -) - -func TestSanity(t *testing.T) { - t.Error("uhh...") -} - -func main() { - fmt.Println("test") -} -`, want: "test"}, - - {prog: ` -package main//comment - -import "fmt" - -func ExampleOutput() { - fmt.Println("The output") - // Output: The output -} -`, want: `=== RUN ExampleOutput ---- PASS: ExampleOutput (0.00s) -PASS`}, - - {prog: ` -package main//comment - -import "fmt" - -func ExampleUnorderedOutput() { - fmt.Println("2") - fmt.Println("1") - fmt.Println("3") - // Unordered output: 3 - // 2 - // 1 -} -`, want: `=== RUN ExampleUnorderedOutput ---- PASS: ExampleUnorderedOutput (0.00s) -PASS`}, - - {prog: ` -package main - -import "fmt" - -func ExampleEmptyOutput() { - // Output: -} - -func ExampleEmptyOutputFail() { - fmt.Println("1") - // Output: -} -`, want: `=== RUN ExampleEmptyOutput ---- PASS: ExampleEmptyOutput (0.00s) -=== RUN ExampleEmptyOutputFail ---- FAIL: ExampleEmptyOutputFail (0.00s) -got: -1 -want: - -FAIL`}, - - // Run program without executing this example function. - {prog: ` -package main - -func ExampleNoOutput() { - panic(1) -} -`, want: `testing: warning: no tests to run -PASS`}, - - {prog: ` -package main - -import "fmt" - -func ExampleShouldNotRun() { - fmt.Println("The output") - // Output: The output -} - -func main() { - fmt.Println("Main") -} -`, want: "Main"}, - - {prog: ` -package main - -import ( - "fmt" - "os" -) - -func main() { - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stderr, "B") - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stdout, "A") -} -`, want: "A\nB\nA\nA\n"}, - - // Integration test for runtime.write fake timestamps. - {prog: ` -package main - -import ( - "fmt" - "os" - "time" -) - -func main() { - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stderr, "B") - fmt.Fprintln(os.Stdout, "A") - fmt.Fprintln(os.Stdout, "A") - time.Sleep(time.Second) - fmt.Fprintln(os.Stderr, "B") - time.Sleep(time.Second) - fmt.Fprintln(os.Stdout, "A") -} -`, wantEvents: []Event{ - {"A\n", "stdout", 0}, - {"B\n", "stderr", time.Nanosecond}, - {"A\nA\n", "stdout", time.Nanosecond}, - {"B\n", "stderr", time.Second - 2*time.Nanosecond}, - {"A\n", "stdout", time.Second}, - }}, - - // Test third-party imports: - {prog: ` -package main -import ("fmt"; "github.com/bradfitz/iter") -func main() { for i := range iter.N(5) { fmt.Println(i) } } -`, want: "0\n1\n2\n3\n4\n"}, -} diff --git a/tests.go b/tests.go new file mode 100644 index 00000000..437da2b3 --- /dev/null +++ b/tests.go @@ -0,0 +1,444 @@ +// Copyright 2014 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. + +// Test tests are linked into the main binary and are run as part of +// the Docker build step. + +package main + +import ( + "fmt" + stdlog "log" + "os" + "reflect" + "strings" + "time" +) + +type compileTest struct { + name string // test name + prog, want, errors string + withVet bool + wantEvents []Event + wantVetErrors string +} + +func (s *server) test() { + if err := s.healthCheck(); err != nil { + stdlog.Fatal(err) + } + + // Enable module downloads for testing: + defer func(old string) { os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", old) }(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) + os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", "true") + + for i, t := range tests { + fmt.Printf("testing case %d (%q)...\n", i, t.name) + resp, err := compileAndRun(&request{Body: t.prog, WithVet: t.withVet}) + if err != nil { + stdlog.Fatal(err) + } + if t.wantEvents != nil { + if !reflect.DeepEqual(resp.Events, t.wantEvents) { + stdlog.Fatalf("resp.Events = %q, want %q", resp.Events, t.wantEvents) + } + continue + } + if t.errors != "" { + if resp.Errors != t.errors { + stdlog.Fatalf("resp.Errors = %q, want %q", resp.Errors, t.errors) + } + continue + } + if resp.Errors != "" { + stdlog.Fatal(resp.Errors) + } + if resp.VetErrors != t.wantVetErrors { + stdlog.Fatalf("resp.VetErrs = %q, want %q", resp.VetErrors, t.wantVetErrors) + } + if len(resp.Events) == 0 { + stdlog.Fatalf("unexpected output: %q, want %q", "", t.want) + } + var b strings.Builder + for _, e := range resp.Events { + b.WriteString(e.Message) + } + if !strings.Contains(b.String(), t.want) { + stdlog.Fatalf("unexpected output: %q, want %q", b.String(), t.want) + } + } + fmt.Println("OK") +} + +var tests = []compileTest{ + { + name: "timezones_available", + prog: ` +package main + +import "time" + +func main() { + loc, err := time.LoadLocation("America/New_York") + if err != nil { + panic(err.Error()) + } + println(loc.String()) +} +`, want: "America/New_York"}, + + { + name: "faketime_works", + prog: ` +package main + +import ( + "fmt" + "time" +) + +func main() { + fmt.Println(time.Now()) +} +`, want: "2009-11-10 23:00:00 +0000 UTC"}, + + { + name: "faketime_tickers", + prog: ` +package main + +import ( + "fmt" + "time" +) + +func main() { + t1 := time.Tick(time.Second * 3) + t2 := time.Tick(time.Second * 7) + t3 := time.Tick(time.Second * 11) + end := time.After(time.Second * 19) + want := "112131211" + var got []byte + for { + var c byte + select { + case <-t1: + c = '1' + case <-t2: + c = '2' + case <-t3: + c = '3' + case <-end: + if g := string(got); g != want { + fmt.Printf("got %q, want %q\n", g, want) + } else { + fmt.Println("timers fired as expected") + } + return + } + got = append(got, c) + } +} +`, want: "timers fired as expected"}, + + { + name: "old_tour_pkgs_in_gopath", + prog: ` +package main + +import ( + "code.google.com/p/go-tour/pic" + "code.google.com/p/go-tour/reader" + "code.google.com/p/go-tour/tree" + "code.google.com/p/go-tour/wc" +) + +var ( + _ = pic.Show + _ = reader.Validate + _ = tree.New + _ = wc.Test +) + +func main() { + println("ok") +} +`, want: "ok"}, + { + name: "must_be_package_main", + prog: ` +package test + +func main() { + println("test") +} +`, want: "", errors: "package name must be main"}, + { + name: "filesystem_contents", + prog: ` +package main + +import ( + "fmt" + "os" + "path/filepath" +) + +func main() { + filepath.Walk("/", func(path string, info os.FileInfo, err error) error { + fmt.Println(path) + return nil + }) +} +`, want: `/ +/dev +/dev/null +/dev/random +/dev/urandom +/dev/zero +/etc +/etc/group +/etc/hosts +/etc/passwd +/etc/resolv.conf +/tmp +/usr +/usr/local +/usr/local/go +/usr/local/go/lib +/usr/local/go/lib/time +/usr/local/go/lib/time/zoneinfo.zip`}, + + { + name: "test_passes", + prog: ` +package main + +import "testing" + +func TestSanity(t *testing.T) { + if 1+1 != 2 { + t.Error("uhh...") + } +} +`, want: `=== RUN TestSanity +--- PASS: TestSanity (0.00s) +PASS`}, + + { + name: "test_without_import", + prog: ` +package main + +func TestSanity(t *testing.T) { + t.Error("uhh...") +} + +func ExampleNotExecuted() { + // Output: it should not run +} +`, want: "", errors: "prog.go:4:20: undefined: testing\n"}, + + { + name: "test_with_import_ignored", + prog: ` +package main + +import ( + "fmt" + "testing" +) + +func TestSanity(t *testing.T) { + t.Error("uhh...") +} + +func main() { + fmt.Println("test") +} +`, want: "test"}, + + { + name: "example_runs", + prog: ` +package main//comment + +import "fmt" + +func ExampleOutput() { + fmt.Println("The output") + // Output: The output +} +`, want: `=== RUN ExampleOutput +--- PASS: ExampleOutput (0.00s) +PASS`}, + + { + name: "example_unordered", + prog: ` +package main//comment + +import "fmt" + +func ExampleUnorderedOutput() { + fmt.Println("2") + fmt.Println("1") + fmt.Println("3") + // Unordered output: 3 + // 2 + // 1 +} +`, want: `=== RUN ExampleUnorderedOutput +--- PASS: ExampleUnorderedOutput (0.00s) +PASS`}, + + { + name: "example_fail", + prog: ` +package main + +import "fmt" + +func ExampleEmptyOutput() { + // Output: +} + +func ExampleEmptyOutputFail() { + fmt.Println("1") + // Output: +} +`, want: `=== RUN ExampleEmptyOutput +--- PASS: ExampleEmptyOutput (0.00s) +=== RUN ExampleEmptyOutputFail +--- FAIL: ExampleEmptyOutputFail (0.00s) +got: +1 +want: + +FAIL`}, + + // Run program without executing this example function. + { + name: "example_no_output_skips_run", + prog: ` +package main + +func ExampleNoOutput() { + panic(1) +} +`, want: `testing: warning: no tests to run +PASS`}, + + { + name: "example_output", + prog: ` +package main + +import "fmt" + +func ExampleShouldNotRun() { + fmt.Println("The output") + // Output: The output +} + +func main() { + fmt.Println("Main") +} +`, want: "Main"}, + + { + name: "stdout_stderr_merge", + prog: ` +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stderr, "B") + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stdout, "A") +} +`, want: "A\nB\nA\nA\n"}, + + // Integration test for runtime.write fake timestamps. + { + name: "faketime_write_interaction", + prog: ` +package main + +import ( + "fmt" + "os" + "time" +) + +func main() { + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stderr, "B") + fmt.Fprintln(os.Stdout, "A") + fmt.Fprintln(os.Stdout, "A") + time.Sleep(time.Second) + fmt.Fprintln(os.Stderr, "B") + time.Sleep(time.Second) + fmt.Fprintln(os.Stdout, "A") +} +`, wantEvents: []Event{ + {"A\n", "stdout", 0}, + {"B\n", "stderr", time.Nanosecond}, + {"A\nA\n", "stdout", time.Nanosecond}, + {"B\n", "stderr", time.Second - 2*time.Nanosecond}, + {"A\n", "stdout", time.Second}, + }}, + + { + name: "third_party_imports", + prog: ` +package main +import ("fmt"; "github.com/bradfitz/iter") +func main() { for i := range iter.N(5) { fmt.Println(i) } } +`, want: "0\n1\n2\n3\n4\n"}, + + { + name: "compile_with_vet", + withVet: true, + wantVetErrors: "prog.go:5:2: Printf format %v reads arg #1, but call has 0 args\n", + prog: ` +package main +import "fmt" +func main() { + fmt.Printf("hi %v") +} +`, + }, + + { + name: "compile_without_vet", + withVet: false, + prog: ` +package main +import "fmt" +func main() { + fmt.Printf("hi %v") +} +`, + }, + + { + name: "compile_modules_with_vet", + withVet: true, + wantVetErrors: "prog.go:6:2: Printf format %v reads arg #1, but call has 0 args\n", + prog: ` +package main +import ("fmt"; "github.com/bradfitz/iter") +func main() { + for i := range iter.N(5) { fmt.Println(i) } + fmt.Printf("hi %v") +} +`, + }, +} diff --git a/vet.go b/vet.go index b4925dbb..78df95c6 100644 --- a/vet.go +++ b/vet.go @@ -16,6 +16,11 @@ import ( // vetCheck runs the "vet" tool on the source code in req.Body. // In case of no errors it returns an empty, non-nil *response. // Otherwise &response.Errors contains found errors. +// +// Deprecated: this is the handler for the legacy /vet endpoint; use +// the /compile (compileAndRun) handler instead with the WithVet +// boolean set. This code path doesn't support modules and only exists +// as a temporary compatiblity bridge to older javascript clients. func vetCheck(req *request) (*response, error) { tmpDir, err := ioutil.TempDir("", "vet") if err != nil { @@ -23,23 +28,44 @@ func vetCheck(req *request) (*response, error) { } defer os.RemoveAll(tmpDir) - in := filepath.Join(tmpDir, "main.go") + in := filepath.Join(tmpDir, progName) if err := ioutil.WriteFile(in, []byte(req.Body), 0400); err != nil { return nil, fmt.Errorf("error creating temp file %q: %v", in, err) } + const useModules = false // legacy handler; no modules (see func comment) + vetOutput, err := vetCheckInDir(tmpDir, os.Getenv("GOPATH"), useModules) + if err != nil { + // This is about errors running vet, not vet returning output. + return nil, err + } + return &response{Errors: vetOutput}, nil +} +// vetCheckInDir runs go vet in the provided directory, using the +// provided GOPATH value, and whether modules are enabled. The +// returned error is only about whether go vet was able to run, not +// whether vet reported problem. The returned value is ("", nil) if +// vet successfully found nothing, and (non-empty, nil) if vet ran and +// found issues. +func vetCheckInDir(dir, goPath string, modules bool) (output string, execErr error) { + in := filepath.Join(dir, progName) cmd := exec.Command("go", "vet", in) // Linux go binary is not built with CGO_ENABLED=0. // Prevent vet to compile packages in cgo mode. // See #26307. - cmd.Env = append(os.Environ(), "CGO_ENABLED=0") + cmd.Env = append(os.Environ(), "CGO_ENABLED=0", "GOPATH="+goPath) + if modules { + cmd.Env = append(cmd.Env, + "GO111MODULE=on", + "GOPROXY=https://proxy.golang.org", + ) + } out, err := cmd.CombinedOutput() if err == nil { - return &response{}, nil + return "", nil } - if _, ok := err.(*exec.ExitError); !ok { - return nil, fmt.Errorf("error vetting go source: %v", err) + return "", fmt.Errorf("error vetting go source: %v", err) } // Rewrite compiler errors to refer to progName @@ -50,5 +76,5 @@ func vetCheck(req *request) (*response, error) { // message before any compile errors; strip it. errs = strings.Replace(errs, "# command-line-arguments\n", "", 1) - return &response{Errors: errs}, nil + return errs, nil } From 1b5b0981c531944b0c31e5c40ced1747d81e77b8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 13 May 2019 21:46:16 +0000 Subject: [PATCH 020/148] playground: enable third-party imports Updates golang/go#31944 Change-Id: Iffc0755cca419b72d8facd8ece950e78cdae892e Reviewed-on: https://go-review.googlesource.com/c/playground/+/176940 Reviewed-by: Andrew Bonventre --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ea9bfea9..e6295da4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -94,7 +94,7 @@ WORKDIR /app RUN /app/playground test # Whether we allow third-party imports via proxy.golang.org: -ENV ALLOW_PLAY_MODULE_DOWNLOADS false +ENV ALLOW_PLAY_MODULE_DOWNLOADS true EXPOSE 8080 ENTRYPOINT ["/app/playground"] From a7b4d4c4bb72efd2ec20c9f30d5c1e6f09cee3d2 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 14 May 2019 19:38:32 +0000 Subject: [PATCH 021/148] playground: support multiple input files in txtar format Updates golang/go#32040 Updates golang/go#31944 (Notably, you can now include a go.mod file) Change-Id: I56846e86d3d98fdf4cac388b5b284dbc187e3b36 Reviewed-on: https://go-review.googlesource.com/c/playground/+/177043 Reviewed-by: Dmitri Shuralyov --- Dockerfile | 5 ++ fmt.go | 51 +++++++++++------ fmt_test.go | 84 +++++++++++++++++++++++++++ go.mod | 1 + go.sum | 8 +++ sandbox.go | 61 ++++++++++++++------ server_test.go | 34 +++++++++++ tests.go | 50 +++++++++++++++- txtar.go | 121 ++++++++++++++++++++++++++++++++++++++ txtar_test.go | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ vet.go | 19 +++--- 11 files changed, 540 insertions(+), 47 deletions(-) create mode 100644 fmt_test.go create mode 100644 txtar.go create mode 100644 txtar_test.go diff --git a/Dockerfile b/Dockerfile index e6295da4..27adf061 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,12 +41,17 @@ RUN mkdir /gocache ENV GOCACHE /gocache ENV GO111MODULE on +COPY go.mod /go/src/playground/go.mod +COPY go.sum /go/src/playground/go.sum +WORKDIR /go/src/playground + # Pre-build some packages to speed final install later. RUN go install cloud.google.com/go/compute/metadata RUN go install cloud.google.com/go/datastore RUN go install github.com/bradfitz/gomemcache/memcache RUN go install golang.org/x/tools/godoc/static RUN go install golang.org/x/tools/imports +RUN go install github.com/rogpeppe/go-internal/txtar # Add and compile playground daemon COPY . /go/src/playground/ diff --git a/fmt.go b/fmt.go index 09f50f26..c5aa9431 100644 --- a/fmt.go +++ b/fmt.go @@ -20,25 +20,40 @@ type fmtResponse struct { } func handleFmt(w http.ResponseWriter, r *http.Request) { - var ( - in = []byte(r.FormValue("body")) - out []byte - err error - ) - if r.FormValue("imports") != "" { - out, err = imports.Process(progName, in, nil) - } else { - out, err = format.Source(in) - } - var resp fmtResponse + w.Header().Set("Content-Type", "application/json") + + fs, err := splitFiles([]byte(r.FormValue("body"))) if err != nil { - resp.Error = err.Error() - // Prefix the error returned by format.Source. - if !strings.HasPrefix(resp.Error, progName) { - resp.Error = fmt.Sprintf("%v:%v", progName, resp.Error) + json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()}) + return + } + + fixImports := r.FormValue("imports") != "" + for _, f := range fs.files { + if !strings.HasSuffix(f, ".go") { + continue } - } else { - resp.Body = string(out) + var out []byte + var err error + in := fs.m[f] + if fixImports { + // TODO: pass options to imports.Process so it + // can find symbols in sibling files. + out, err = imports.Process(progName, in, nil) + } else { + out, err = format.Source(in) + } + if err != nil { + errMsg := err.Error() + // Prefix the error returned by format.Source. + if !strings.HasPrefix(errMsg, f) { + errMsg = fmt.Sprintf("%v:%v", f, errMsg) + } + json.NewEncoder(w).Encode(fmtResponse{Error: errMsg}) + return + } + fs.AddFile(f, out) } - json.NewEncoder(w).Encode(resp) + + json.NewEncoder(w).Encode(fmtResponse{Body: string(fs.Format())}) } diff --git a/fmt_test.go b/fmt_test.go new file mode 100644 index 00000000..b8a1b9ab --- /dev/null +++ b/fmt_test.go @@ -0,0 +1,84 @@ +// 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. + +package main + +import ( + "encoding/json" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func TestHandleFmt(t *testing.T) { + for _, tt := range []struct { + name string + body string + imports bool + want string + wantErr string + }{ + { + name: "classic", + body: " package main\n func main( ) { }\n", + want: "package main\n\nfunc main() {}\n", + }, + { + name: "classic_goimports", + body: " package main\nvar _ = fmt.Printf", + imports: true, + want: "package main\n\nimport \"fmt\"\n\nvar _ = fmt.Printf\n", + }, + { + name: "single_go_with_header", + body: "-- prog.go --\n package main", + want: "-- prog.go --\npackage main\n", + }, + { + name: "multi_go_with_header", + body: "-- prog.go --\n package main\n\n\n-- two.go --\n package main\n var X = 5", + want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n", + }, + { + name: "multi_go_without_header", + body: " package main\n\n\n-- two.go --\n package main\n var X = 5", + want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n", + }, + { + name: "only_format_go", + body: " package main\n\n\n-- go.mod --\n module foo\n", + want: "package main\n-- go.mod --\n module foo\n", + }, + } { + t.Run(tt.name, func(t *testing.T) { + rec := httptest.NewRecorder() + form := url.Values{} + form.Set("body", tt.body) + if tt.imports { + form.Set("imports", "true") + } + req := httptest.NewRequest("POST", "/fmt", strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + handleFmt(rec, req) + resp := rec.Result() + if resp.StatusCode != 200 { + t.Fatalf("code = %v", resp.Status) + } + if ct := resp.Header.Get("Content-Type"); ct != "application/json" { + t.Fatalf("Content-Type = %q; want application/json", ct) + } + var got fmtResponse + if err := json.NewDecoder(resp.Body).Decode(&got); err != nil { + t.Fatal(err) + } + if got.Body != tt.want { + t.Errorf("wrong output\n got: %q\nwant: %q\n", got.Body, tt.want) + } + if got.Error != tt.wantErr { + t.Errorf("wrong error\n got err: %q\nwant err: %q\n", got.Error, tt.wantErr) + } + }) + } +} diff --git a/go.mod b/go.mod index f68be743..be07315e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( cloud.google.com/go v0.38.0 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 + github.com/rogpeppe/go-internal v1.3.0 golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 ) diff --git a/go.sum b/go.sum index 8e9d91d0..8a315b3b 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,12 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -69,5 +75,7 @@ google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCP google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/sandbox.go b/sandbox.go index 56e96136..c4a865d4 100644 --- a/sandbox.go +++ b/sandbox.go @@ -311,33 +311,57 @@ func compileAndRun(req *request) (*response, error) { } defer os.RemoveAll(tmpDir) - src := []byte(req.Body) - in := filepath.Join(tmpDir, progName) - if err := ioutil.WriteFile(in, src, 0400); err != nil { - return nil, fmt.Errorf("error creating temp file %q: %v", in, err) + files, err := splitFiles([]byte(req.Body)) + if err != nil { + return &response{Errors: err.Error()}, nil } - fset := token.NewFileSet() + var testParam string + var buildPkgArg = "." + if files.Num() == 1 && len(files.Data(progName)) > 0 { + buildPkgArg = progName + src := files.Data(progName) + if code := getTestProg(src); code != nil { + testParam = "-test.v" + files.AddFile(progName, code) + } + } - f, err := parser.ParseFile(fset, in, nil, parser.PackageClauseOnly) - if err == nil && f.Name.Name != "main" { - return &response{Errors: "package name must be main"}, nil + useModules := allowModuleDownloads(files) + if !files.Contains("go.mod") && useModules { + files.AddFile("go.mod", []byte("module play\n")) } - var testParam string - if code := getTestProg(src); code != nil { - testParam = "-test.v" - if err := ioutil.WriteFile(in, code, 0400); err != nil { + for f, src := range files.m { + // Before multi-file support we required that the + // program be in package main, so continue to do that + // for now. But permit anything in subdirectories to have other + // packages. + if !strings.Contains(f, "/") { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly) + if err == nil && f.Name.Name != "main" { + return &response{Errors: "package name must be main"}, nil + } + } + + in := filepath.Join(tmpDir, f) + if strings.Contains(f, "/") { + if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil { + return nil, err + } + } + if err := ioutil.WriteFile(in, src, 0644); err != nil { return nil, fmt.Errorf("error creating temp file %q: %v", in, err) } } exe := filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") - cmd := exec.Command("go", "build", "-o", exe, in) + cmd := exec.Command("go", "build", "-o", exe, buildPkgArg) + cmd.Dir = tmpDir var goPath string cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache} - useModules := allowModuleDownloads(src) if useModules { // Create a GOPATH just for modules to be downloaded // into GOPATH/pkg/mod. @@ -356,9 +380,8 @@ func compileAndRun(req *request) (*response, error) { if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. - // Rewrite compiler errors to refer to progName - // instead of '/tmp/sandbox1234/prog.go'. - errs := strings.Replace(string(out), in, progName, -1) + // Rewrite compiler errors to strip the tmpDir name. + errs := strings.Replace(string(out), tmpDir+"/", "", -1) // "go build", invoked with a file name, puts this odd // message before any compile errors; strip it. @@ -422,8 +445,8 @@ func compileAndRun(req *request) (*response, error) { // allowModuleDownloads reports whether the code snippet in src should be allowed // to download modules. -func allowModuleDownloads(src []byte) bool { - if bytes.Contains(src, []byte(`"code.google.com/p/go-tour/`)) { +func allowModuleDownloads(files *fileSet) bool { + if files.Num() == 1 && bytes.Contains(files.Data(progName), []byte(`"code.google.com/p/go-tour/`)) { // This domain doesn't exist anymore but we want old snippets using // these packages to still run, so the Dockerfile adds these packages // at this name in $GOPATH. Any snippets using this old name wouldn't diff --git a/server_test.go b/server_test.go index 4ff2f034..9cc19da0 100644 --- a/server_test.go +++ b/server_test.go @@ -1,6 +1,7 @@ // Copyright 2017 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. + package main import ( @@ -10,6 +11,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "os" "testing" ) @@ -240,3 +242,35 @@ func TestCommandHandler(t *testing.T) { } } } + +func TestAllowModuleDownloads(t *testing.T) { + const envKey = "ALLOW_PLAY_MODULE_DOWNLOADS" + defer func(old string) { os.Setenv(envKey, old) }(os.Getenv(envKey)) + + tests := []struct { + src string + env string + want bool + }{ + {src: "package main", want: true}, + {src: "package main", env: "false", want: false}, + {src: `import "code.google.com/p/go-tour/"`, want: false}, + } + for i, tt := range tests { + if tt.env != "" { + os.Setenv(envKey, tt.env) + } else { + os.Setenv(envKey, "true") + } + files, err := splitFiles([]byte(tt.src)) + if err != nil { + t.Errorf("%d. splitFiles = %v", i, err) + continue + } + got := allowModuleDownloads(files) + if got != tt.want { + t.Errorf("%d. allow = %v; want %v; files:\n%s", i, got, tt.want, filesAsString(files)) + } + } + +} diff --git a/tests.go b/tests.go index 437da2b3..18f6ea0a 100644 --- a/tests.go +++ b/tests.go @@ -57,6 +57,9 @@ func (s *server) test() { if resp.VetErrors != t.wantVetErrors { stdlog.Fatalf("resp.VetErrs = %q, want %q", resp.VetErrors, t.wantVetErrors) } + if t.withVet && (resp.VetErrors != "") == resp.VetOK { + stdlog.Fatalf("resp.VetErrs & VetOK inconsistent; VetErrs = %q; VetOK = %v", resp.VetErrors, resp.VetOK) + } if len(resp.Events) == 0 { stdlog.Fatalf("unexpected output: %q, want %q", "", t.want) } @@ -238,7 +241,7 @@ func TestSanity(t *testing.T) { func ExampleNotExecuted() { // Output: it should not run } -`, want: "", errors: "prog.go:4:20: undefined: testing\n"}, +`, want: "", errors: "./prog.go:4:20: undefined: testing\n"}, { name: "test_with_import_ignored", @@ -406,7 +409,7 @@ func main() { for i := range iter.N(5) { fmt.Println(i) } } { name: "compile_with_vet", withVet: true, - wantVetErrors: "prog.go:5:2: Printf format %v reads arg #1, but call has 0 args\n", + wantVetErrors: "./prog.go:5:2: Printf format %v reads arg #1, but call has 0 args\n", prog: ` package main import "fmt" @@ -431,7 +434,7 @@ func main() { { name: "compile_modules_with_vet", withVet: true, - wantVetErrors: "prog.go:6:2: Printf format %v reads arg #1, but call has 0 args\n", + wantVetErrors: "./prog.go:6:2: Printf format %v reads arg #1, but call has 0 args\n", prog: ` package main import ("fmt"; "github.com/bradfitz/iter") @@ -439,6 +442,47 @@ func main() { for i := range iter.N(5) { fmt.Println(i) } fmt.Printf("hi %v") } +`, + }, + + { + name: "multi_file_basic", + prog: ` +package main +const foo = "bar" + +-- two.go -- +package main +func main() { + println(foo) +} +`, + wantEvents: []Event{ + {"bar\n", "stderr", 0}, + }, + }, + + { + name: "multi_file_use_package", + withVet: true, + prog: ` +package main + +import "play.test/foo" + +func main() { + foo.Hello() +} + +-- go.mod -- +module play.test + +-- foo/foo.go -- +package foo + +import "fmt" + +func Hello() { fmt.Println("hello world") } `, }, } diff --git a/txtar.go b/txtar.go new file mode 100644 index 00000000..9b699bf0 --- /dev/null +++ b/txtar.go @@ -0,0 +1,121 @@ +// 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. + +package main + +import ( + "bytes" + "errors" + "fmt" + "path" + "strings" + + "github.com/rogpeppe/go-internal/txtar" +) + +// fileSet is a set of files. +// The zero value for fileSet is an empty set ready to use. +type fileSet struct { + files []string // filenames in user-provided order + m map[string][]byte // filename -> source + noHeader bool // whether the prog.go entry was implicit +} + +// Data returns the content of the named file. +// The fileSet retains ownership of the returned slice. +func (fs *fileSet) Data(filename string) []byte { return fs.m[filename] } + +// Num returns the number of files in the set. +func (fs *fileSet) Num() int { return len(fs.m) } + +// Contains reports whether fs contains the given filename. +func (fs *fileSet) Contains(filename string) bool { + _, ok := fs.m[filename] + return ok +} + +// AddFile adds a file to fs. If fs already contains filename, its +// contents are replaced. +func (fs *fileSet) AddFile(filename string, src []byte) { + had := fs.Contains(filename) + if fs.m == nil { + fs.m = make(map[string][]byte) + } + fs.m[filename] = src + if !had { + fs.files = append(fs.files, filename) + } +} + +// Format returns fs formatted as a txtar archive. +func (fs *fileSet) Format() []byte { + a := new(txtar.Archive) + if fs.noHeader { + a.Comment = fs.m[progName] + } + for i, f := range fs.files { + if i == 0 && f == progName && fs.noHeader { + continue + } + a.Files = append(a.Files, txtar.File{Name: f, Data: fs.m[f]}) + } + return txtar.Format(a) +} + +// splitFiles splits the user's input program src into 1 or more +// files, splitting it based on boundaries as specified by the "txtar" +// format. It returns an error if any filenames are bogus or +// duplicates. The implicit filename for the txtar comment (the lines +// before any txtar separator line) are named "prog.go". It is an +// error to have an explicit file named "prog.go" in addition to +// having the implicit "prog.go" file (non-empty comment section). +// +// The filenames are validated to only be relative paths, not too +// long, not too deep, not have ".." elements, not have backslashes or +// low ASCII binary characters, and to be in path.Clean canonical +// form. +// +// splitFiles takes ownership of src. +func splitFiles(src []byte) (*fileSet, error) { + fs := new(fileSet) + a := txtar.Parse(src) + if v := bytes.TrimSpace(a.Comment); len(v) > 0 { + fs.noHeader = true + fs.AddFile(progName, a.Comment) + } + const limitNumFiles = 20 // arbitrary + numFiles := len(a.Files) + fs.Num() + if numFiles > limitNumFiles { + return nil, fmt.Errorf("too many files in txtar archive (%v exceeds limit of %v)", numFiles, limitNumFiles) + } + for _, f := range a.Files { + if len(f.Name) > 200 { // arbitrary limit + return nil, errors.New("file name too long") + } + if strings.IndexFunc(f.Name, isBogusFilenameRune) != -1 { + return nil, fmt.Errorf("invalid file name %q", f.Name) + } + if f.Name != path.Clean(f.Name) || path.IsAbs(f.Name) { + return nil, fmt.Errorf("invalid file name %q", f.Name) + } + parts := strings.Split(f.Name, "/") + if len(parts) > 10 { // arbitrary limit + return nil, fmt.Errorf("file name %q too deep", f.Name) + } + for _, part := range parts { + if part == "." || part == ".." { + return nil, fmt.Errorf("invalid file name %q", f.Name) + } + } + if fs.Contains(f.Name) { + return nil, fmt.Errorf("duplicate file name %q", f.Name) + } + fs.AddFile(f.Name, f.Data) + } + return fs, nil +} + +// isBogusFilenameRune reports whether r should be rejected if it +// appears in a txtar section's filename. +func isBogusFilenameRune(r rune) bool { return r == '\\' || r < ' ' } diff --git a/txtar_test.go b/txtar_test.go new file mode 100644 index 00000000..ae1ef967 --- /dev/null +++ b/txtar_test.go @@ -0,0 +1,153 @@ +// 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. + +package main + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func newFileSet(kv ...string) *fileSet { + fs := new(fileSet) + if kv[0] == "prog.go!implicit" { + fs.noHeader = true + kv[0] = "prog.go" + } + for len(kv) > 0 { + fs.AddFile(kv[0], []byte(kv[1])) + kv = kv[2:] + } + return fs +} + +func TestSplitFiles(t *testing.T) { + for _, tt := range []struct { + name string + in string + want *fileSet + wantErr string + }{ + { + name: "classic", + in: "package main", + want: newFileSet("prog.go!implicit", "package main\n"), + }, + { + name: "implicit prog.go", + in: "package main\n-- two.go --\nsecond", + want: newFileSet( + "prog.go!implicit", "package main\n", + "two.go", "second\n", + ), + }, + { + name: "basic txtar", + in: "-- main.go --\npackage main\n-- foo.go --\npackage main\n", + want: newFileSet( + "main.go", "package main\n", + "foo.go", "package main\n", + ), + }, + { + name: "reject dotdot 1", + in: "-- ../foo --\n", + wantErr: `invalid file name "../foo"`, + }, + { + name: "reject dotdot 2", + in: "-- .. --\n", + wantErr: `invalid file name ".."`, + }, + { + name: "reject dotdot 3", + in: "-- bar/../foo --\n", + wantErr: `invalid file name "bar/../foo"`, + }, + { + name: "reject long", + in: "-- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx --\n", + wantErr: `file name too long`, + }, + { + name: "reject deep", + in: "-- x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x --\n", + wantErr: `file name "x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x" too deep`, + }, + { + name: "reject abs", + in: "-- /etc/passwd --\n", + wantErr: `invalid file name "/etc/passwd"`, + }, + { + name: "reject backslash", + in: "-- foo\\bar --\n", + wantErr: `invalid file name "foo\\bar"`, + }, + { + name: "reject binary null", + in: "-- foo\x00bar --\n", + wantErr: `invalid file name "foo\x00bar"`, + }, + { + name: "reject binary low", + in: "-- foo\x1fbar --\n", + wantErr: `invalid file name "foo\x1fbar"`, + }, + { + name: "reject dup", + in: "-- foo.go --\n-- foo.go --\n", + wantErr: `duplicate file name "foo.go"`, + }, + { + name: "reject implicit dup", + in: "package main\n-- prog.go --\n", + wantErr: `duplicate file name "prog.go"`, + }, + { + name: "skip leading whitespace comment", + in: "\n \n\n \n\n-- f.go --\ncontents", + want: newFileSet("f.go", "contents\n"), + }, + { + name: "reject many files", + in: strings.Repeat("-- x.go --\n", 50), + wantErr: `too many files in txtar archive (50 exceeds limit of 20)`, + }, + } { + got, err := splitFiles([]byte(tt.in)) + var gotErr string + if err != nil { + gotErr = err.Error() + } + if gotErr != tt.wantErr { + if tt.wantErr == "" { + t.Errorf("%s: unexpected error: %v", tt.name, err) + continue + } + t.Errorf("%s: error = %#q; want error %#q", tt.name, err, tt.wantErr) + continue + } + if err != nil { + continue + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("%s: wrong files\n got:\n%s\nwant:\n%s", tt.name, filesAsString(got), filesAsString(tt.want)) + } + } +} + +func filesAsString(fs *fileSet) string { + var sb strings.Builder + for i, f := range fs.files { + var implicit string + if i == 0 && f == progName && fs.noHeader { + implicit = " (implicit)" + } + fmt.Fprintf(&sb, "[file %q%s]: %q\n", f, implicit, fs.m[f]) + } + return sb.String() +} diff --git a/vet.go b/vet.go index 78df95c6..32edeca0 100644 --- a/vet.go +++ b/vet.go @@ -48,8 +48,11 @@ func vetCheck(req *request) (*response, error) { // vet successfully found nothing, and (non-empty, nil) if vet ran and // found issues. func vetCheckInDir(dir, goPath string, modules bool) (output string, execErr error) { - in := filepath.Join(dir, progName) - cmd := exec.Command("go", "vet", in) + cmd := exec.Command("go", "vet") + if !modules { + cmd.Args = append(cmd.Args, progName) + } + cmd.Dir = dir // Linux go binary is not built with CGO_ENABLED=0. // Prevent vet to compile packages in cgo mode. // See #26307. @@ -70,11 +73,13 @@ func vetCheckInDir(dir, goPath string, modules bool) (output string, execErr err // Rewrite compiler errors to refer to progName // instead of '/tmp/sandbox1234/main.go'. - errs := strings.Replace(string(out), in, progName, -1) - - // "go vet", invoked with a file name, puts this odd - // message before any compile errors; strip it. - errs = strings.Replace(errs, "# command-line-arguments\n", "", 1) + errs := strings.Replace(string(out), dir, "", -1) + // Remove vet's package name banner. + if strings.HasPrefix(errs, "#") { + if nl := strings.Index(errs, "\n"); nl != -1 { + errs = errs[nl+1:] + } + } return errs, nil } From e1479bc33ebe3034511f953ef8582f1c7aae9d0e Mon Sep 17 00:00:00 2001 From: Dmitri Shuralyov Date: Wed, 15 May 2019 17:13:24 -0400 Subject: [PATCH 022/148] playground: format go.mod files; fix paths in formatting errors Previously, handleFmt applied gofmt/goimports formatting to .go files only. This change makes it apply formatting to go.mod files as well. The cmd/go/internal/modfile package (well, a non-internal copy thereof) is used to perform the go.mod file formatting. Add test cases for error messages, and fix some cases where the paths weren't accurate. Detect when the error was returned by format.Source and needs to be prefixed by using the fixImports variable instead of checking for the presence of the prefix. This makes the code simpler and more readable. Replace old fs.m[f] usage with fs.Data(f) in fmt.go and txtar_test.go. Updates golang/go#32040 Updates golang/go#31944 Change-Id: Iefef7337f962914817558bcf0c622a952160ac44 Reviewed-on: https://go-review.googlesource.com/c/playground/+/177421 Reviewed-by: Brad Fitzpatrick --- Dockerfile | 1 + fmt.go | 61 ++++++++++++++++++++++++++++++++------------------- fmt_test.go | 50 ++++++++++++++++++++++++++++++++++++++--- txtar_test.go | 2 +- 4 files changed, 88 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 27adf061..9b3c852c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,6 +51,7 @@ RUN go install cloud.google.com/go/datastore RUN go install github.com/bradfitz/gomemcache/memcache RUN go install golang.org/x/tools/godoc/static RUN go install golang.org/x/tools/imports +RUN go install github.com/rogpeppe/go-internal/modfile RUN go install github.com/rogpeppe/go-internal/txtar # Add and compile playground daemon diff --git a/fmt.go b/fmt.go index c5aa9431..002bd34b 100644 --- a/fmt.go +++ b/fmt.go @@ -9,8 +9,9 @@ import ( "fmt" "go/format" "net/http" - "strings" + "path" + "github.com/rogpeppe/go-internal/modfile" "golang.org/x/tools/imports" ) @@ -30,30 +31,46 @@ func handleFmt(w http.ResponseWriter, r *http.Request) { fixImports := r.FormValue("imports") != "" for _, f := range fs.files { - if !strings.HasSuffix(f, ".go") { - continue - } - var out []byte - var err error - in := fs.m[f] - if fixImports { - // TODO: pass options to imports.Process so it - // can find symbols in sibling files. - out, err = imports.Process(progName, in, nil) - } else { - out, err = format.Source(in) - } - if err != nil { - errMsg := err.Error() - // Prefix the error returned by format.Source. - if !strings.HasPrefix(errMsg, f) { - errMsg = fmt.Sprintf("%v:%v", f, errMsg) + switch { + case path.Ext(f) == ".go": + var out []byte + var err error + in := fs.Data(f) + if fixImports { + // TODO: pass options to imports.Process so it + // can find symbols in sibling files. + out, err = imports.Process(f, in, nil) + } else { + out, err = format.Source(in) + } + if err != nil { + errMsg := err.Error() + if !fixImports { + // Unlike imports.Process, format.Source does not prefix + // the error with the file path. So, do it ourselves here. + errMsg = fmt.Sprintf("%v:%v", f, errMsg) + } + json.NewEncoder(w).Encode(fmtResponse{Error: errMsg}) + return + } + fs.AddFile(f, out) + case path.Base(f) == "go.mod": + out, err := formatGoMod(f, fs.Data(f)) + if err != nil { + json.NewEncoder(w).Encode(fmtResponse{Error: err.Error()}) + return } - json.NewEncoder(w).Encode(fmtResponse{Error: errMsg}) - return + fs.AddFile(f, out) } - fs.AddFile(f, out) } json.NewEncoder(w).Encode(fmtResponse{Body: string(fs.Format())}) } + +func formatGoMod(file string, data []byte) ([]byte, error) { + f, err := modfile.Parse(file, data, nil) + if err != nil { + return nil, err + } + return f.Format() +} diff --git a/fmt_test.go b/fmt_test.go index b8a1b9ab..e601dc3d 100644 --- a/fmt_test.go +++ b/fmt_test.go @@ -47,9 +47,53 @@ func TestHandleFmt(t *testing.T) { want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n", }, { - name: "only_format_go", - body: " package main\n\n\n-- go.mod --\n module foo\n", - want: "package main\n-- go.mod --\n module foo\n", + name: "single_go.mod_with_header", + body: "-- go.mod --\n module \"foo\" ", + want: "-- go.mod --\nmodule foo\n", + }, + { + name: "multi_go.mod_with_header", + body: "-- a/go.mod --\n module foo\n\n\n-- b/go.mod --\n module \"bar\"", + want: "-- a/go.mod --\nmodule foo\n-- b/go.mod --\nmodule bar\n", + }, + { + name: "only_format_go_and_go.mod", + body: " package main \n\n\n" + + "-- go.mod --\n module foo \n\n\n" + + "-- plain.txt --\n plain text \n\n\n", + want: "package main\n-- go.mod --\nmodule foo\n-- plain.txt --\n plain text \n\n\n", + }, + { + name: "error_gofmt", + body: "package 123\n", + wantErr: "prog.go:1:9: expected 'IDENT', found 123", + }, + { + name: "error_gofmt_with_header", + body: "-- dir/one.go --\npackage 123\n", + wantErr: "dir/one.go:1:9: expected 'IDENT', found 123", + }, + { + name: "error_goimports", + body: "package 123\n", + imports: true, + wantErr: "prog.go:1:9: expected 'IDENT', found 123", + }, + { + name: "error_goimports_with_header", + body: "-- dir/one.go --\npackage 123\n", + imports: true, + wantErr: "dir/one.go:1:9: expected 'IDENT', found 123", + }, + { + name: "error_go.mod", + body: "-- go.mod --\n123\n", + wantErr: "go.mod:1: unknown directive: 123", + }, + { + name: "error_go.mod_with_header", + body: "-- dir/go.mod --\n123\n", + wantErr: "dir/go.mod:1: unknown directive: 123", }, } { t.Run(tt.name, func(t *testing.T) { diff --git a/txtar_test.go b/txtar_test.go index ae1ef967..493d140f 100644 --- a/txtar_test.go +++ b/txtar_test.go @@ -147,7 +147,7 @@ func filesAsString(fs *fileSet) string { if i == 0 && f == progName && fs.noHeader { implicit = " (implicit)" } - fmt.Fprintf(&sb, "[file %q%s]: %q\n", f, implicit, fs.m[f]) + fmt.Fprintf(&sb, "[file %q%s]: %q\n", f, implicit, fs.Data(f)) } return sb.String() } From 742871fe54f1361dec093509e7de27fe92f23743 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 15 May 2019 22:29:36 +0000 Subject: [PATCH 023/148] Makefile: adjust the test rule to be more useful Run quick tests before the slow tests. Change-Id: I52d0d9a72de416201745eff992d84d72409cecae Reviewed-on: https://go-review.googlesource.com/c/playground/+/177477 Reviewed-by: Dmitri Shuralyov --- Makefile | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 3fbb2ed4..4fe2ee71 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ -.PHONY: update-deps docker test +.PHONY: docker test -docker: Dockerfile - docker build -t playground . +docker: + docker build -t golang/playground . -test: docker - go test - docker run --rm playground test +test: + # Run fast tests first: (and tests whether, say, things compile) + GO111MODULE=on go test -v + # Then run the slower tests, which happen as one of the + # Dockerfile RUN steps: + docker build -t golang/playground . From c4ae04c716a40dc51f5facb75567153f008c7b02 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Thu, 13 Jun 2019 14:46:35 -0400 Subject: [PATCH 024/148] playground: parameterize GO_VERSION in Dockerfile This will allow us to specify a GO_VERSION at build time. Bumps GO_VERSION to go1.12.6. Moves BUILD_DEPS install step before GO_VERSION which improves intermediate step caching during development. Verified locally with: - docker build --build-arg GO_VERSION=go1.12.6 (and others) - Ran fmt.Println(runtime.Version()) in the playground to verify the version changed. Updates golang/go#32606 Change-Id: Ic57f8a0a3465d7ea3438a31eab5ca90f85333af5 Reviewed-on: https://go-review.googlesource.com/c/playground/+/182180 Reviewed-by: Dmitri Shuralyov Run-TryBot: Alexander Rakoczy --- Dockerfile | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9b3c852c..3ea5419e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,24 +9,25 @@ RUN apt-get update && apt-get install -y --no-install-recommends curl bzip2 ca-c RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 FROM debian:stretch AS build -LABEL maintainer "golang-dev@googlegroups.com" +LABEL maintainer="golang-dev@googlegroups.com" + +ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' +RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap -ENV GO_VERSION 1.12.5 -ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' +ARG GO_VERSION=go1.12.6 +ENV GO_VERSION ${GO_VERSION} # Fake time COPY enable-fake-time.patch /usr/local/playground/ # Fake file system COPY fake_fs.lst /usr/local/playground/ -RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends - # Get the Go binary. -RUN curl -sSL https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz -o /tmp/go.tar.gz -RUN curl -sSL https://dl.google.com/go/go$GO_VERSION.linux-amd64.tar.gz.sha256 -o /tmp/go.tar.gz.sha256 +RUN curl -sSL https://dl.google.com/go/$GO_VERSION.linux-amd64.tar.gz -o /tmp/go.tar.gz +RUN curl -sSL https://dl.google.com/go/$GO_VERSION.linux-amd64.tar.gz.sha256 -o /tmp/go.tar.gz.sha256 RUN echo "$(cat /tmp/go.tar.gz.sha256) /tmp/go.tar.gz" | sha256sum -c - RUN tar -C /usr/local/ -vxzf /tmp/go.tar.gz # Make a copy for GOROOT_BOOTSTRAP, because we rebuild the toolchain and make.bash removes bin/go as its first step. From 78fe0244324ac1f668ad88b0c1f2d8e3c09bfc02 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 19 Jun 2019 12:54:59 -0400 Subject: [PATCH 025/148] playground: trigger playground releases from Go releases This change includes configuration to build and deploy new releases of the Go playground when a new tag of Go is released to Github. The trigger is configured to filter tags to new, non-rc, non-beta releases of Go. This first commit simply implements releasing the current version of Go configured for the playground. Changes to the build process to ensure we're releasing the latest version of Go will be in a follow-up commit. Updates golang/go#32606 Change-Id: I3b518fd3f02efcd8f510fd865f3370bea19f0e9b Reviewed-on: https://go-review.googlesource.com/c/playground/+/183037 Reviewed-by: Dmitri Shuralyov --- Makefile | 12 +++++++++- README.md | 28 ++++++++++++++++------- cloudbuild_trigger.json | 50 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 cloudbuild_trigger.json diff --git a/Makefile b/Makefile index 4fe2ee71..b0c86516 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,8 @@ -.PHONY: docker test +CLOUDBUILD_TRIGGER_ID := $(shell jq -r .id cloudbuild_trigger.json) +CLOUDBUILD_TRIGGER_JSON := cloudbuild_trigger.json +GCLOUD_ACCESS_TOKEN := $(shell gcloud auth print-access-token) + +.PHONY: docker test update-cloudbuild-trigger docker: docker build -t golang/playground . @@ -9,3 +13,9 @@ test: # Then run the slower tests, which happen as one of the # Dockerfile RUN steps: docker build -t golang/playground . + +update-cloudbuild-trigger: + # The gcloud CLI doesn't yet support updating a trigger. + curl -H "Authorization: Bearer $(GCLOUD_ACCESS_TOKEN)" -H "Content-Type: application/json" \ + -d @$(CLOUDBUILD_TRIGGER_JSON) \ + -X PATCH https://cloudbuild.googleapis.com/v1/projects/golang-org/triggers/$(CLOUDBUILD_TRIGGER_ID) diff --git a/README.md b/README.md index 33bdcfc1..fffee73f 100644 --- a/README.md +++ b/README.md @@ -5,30 +5,31 @@ https://play.golang.org/ ## Building -``` +```bash # build the image docker build -t playground . ``` ## Running -``` +```bash docker run --name=play --rm -d -p 8080:8080 playground # run some Go code cat /path/to/code.go | go run client.go | curl -s --upload-file - localhost:8080/compile ``` -# Deployment +## Deployment -Building the playground Docker container takes more than the default 10 minute time limit of cloud build, so increase its timeout first (note, `app/cloud_build_timeout` is a global configuration value): +Building the playground Docker container takes more than the default 10 minute time limit of cloud build, so increase +its timeout first (note, `app/cloud_build_timeout` is a global configuration value): -``` +```bash gcloud config set app/cloud_build_timeout 1200 # 20 mins ``` Alternatively, to avoid Cloud Build and build locally: -``` +```bash make docker docker tag playground:latest gcr.io/golang-org/playground:latest docker push gcr.io/golang-org/playground:latest @@ -37,11 +38,22 @@ gcloud --project=golang-org --account=you@google.com app deploy app.yaml --image Then: -``` +```bash gcloud --project=golang-org --account=you@google.com app deploy app.yaml ``` -# Contributing +### Deployment Triggers + +Playground releases are also triggered when new tags are pushed to Github. The Cloud Build trigger configuration is +defined in [cloudbuild_trigger.json](cloudbuild_trigger.json). + +Triggers can be updated by running the following Make target: + +```bash +make update-cloudbuild-trigger +``` + +## Contributing To submit changes to this repository, see https://golang.org/doc/contribute.html. diff --git a/cloudbuild_trigger.json b/cloudbuild_trigger.json new file mode 100644 index 00000000..2fa83048 --- /dev/null +++ b/cloudbuild_trigger.json @@ -0,0 +1,50 @@ +{ + "id": "5a2c9e25-a71a-4adf-a785-76c3eca2ac8a", + "description": "Go repository release trigger for x/playground", + "github": { + "name": "go", + "owner": "golang", + "push": { + "tag": "^go[0-9](\\.[0-9]+)+$" + } + }, + "build": { + "steps": [ + { + "name": "gcr.io/cloud-builders/git", + "args": [ + "clone", + "--depth", + "1", + "https://go.googlesource.com/playground" + ] + }, + { + "dir": "playground", + "name": "gcr.io/cloud-builders/docker", + "args": [ + "build", + "-t", + "gcr.io/$PROJECT_ID/playground", + "." + ], + "timeout": "1800s" + }, + { + "dir": "playground", + "name": "gcr.io/cloud-builders/gcloud", + "args": [ + "app", + "deploy", + "app.yaml", + "--project=$PROJECT_ID", + "--image-url=gcr.io/$PROJECT_ID/playground:latest" + ], + "timeout": "1200s" + } + ], + "images": [ + "gcr.io/$PROJECT_ID/playground" + ] + } +} From 607f1dfb1a2b976115af86da310d39a630f7d887 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Fri, 21 Jun 2019 16:09:14 -0400 Subject: [PATCH 026/148] playground: build playground Go from source This changes the playground Dockerfile to build playground from source. Cloud Builds for playground are triggered when a new Go release is tagged. Go release tags are published to repositories before binary releases are published. This change fetches the tagged source in order to build Go, as a tagged binary release may not be available yet. A binary Go release is still downloaded to be used as the GOROOT_BOOTSTRAP. The build steps use maintq to fetch the latest version of Go. maintq currently only outputs in textproto format, requiring a step to parse the git tag. It might be worth adding a flag or special command to maintq to simplify the output to clarify this step of the release process. It might also be worthwhile to move this step into the Makefile, or a checked-in build script. The trigger steps were tested on Cloud Build without the deployment step. Updates golang/go#32606 Change-Id: I811be6aa6b5fb53fce4491e4b4ed235a0f3e62cb Reviewed-on: https://go-review.googlesource.com/c/playground/+/183403 Reviewed-by: Dmitri Shuralyov --- Dockerfile | 19 +++++++++++-------- cloudbuild_trigger.json | 30 ++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3ea5419e..7ddad2a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap +ENV GO_BOOTSTRAP_VERSION go1.12.6 ARG GO_VERSION=go1.12.6 ENV GO_VERSION ${GO_VERSION} @@ -25,17 +26,19 @@ COPY enable-fake-time.patch /usr/local/playground/ # Fake file system COPY fake_fs.lst /usr/local/playground/ -# Get the Go binary. -RUN curl -sSL https://dl.google.com/go/$GO_VERSION.linux-amd64.tar.gz -o /tmp/go.tar.gz -RUN curl -sSL https://dl.google.com/go/$GO_VERSION.linux-amd64.tar.gz.sha256 -o /tmp/go.tar.gz.sha256 +# Get a bootstrap version of Go for building from source. +RUN curl -sSL https://dl.google.com/go/$GO_BOOTSTRAP_VERSION.linux-amd64.tar.gz -o /tmp/go.tar.gz +RUN curl -sSL https://dl.google.com/go/$GO_BOOTSTRAP_VERSION.linux-amd64.tar.gz.sha256 -o /tmp/go.tar.gz.sha256 RUN echo "$(cat /tmp/go.tar.gz.sha256) /tmp/go.tar.gz" | sha256sum -c - -RUN tar -C /usr/local/ -vxzf /tmp/go.tar.gz -# Make a copy for GOROOT_BOOTSTRAP, because we rebuild the toolchain and make.bash removes bin/go as its first step. -RUN cp -R /usr/local/go $GOROOT_BOOTSTRAP +RUN mkdir -p $GOROOT_BOOTSTRAP +RUN tar --strip=1 -C $GOROOT_BOOTSTRAP -vxzf /tmp/go.tar.gz + +# Fetch Go source for tag $GO_VERSION. +RUN git clone --depth=1 --branch=$GO_VERSION https://go.googlesource.com/go /usr/local/go # Apply the fake time and fake filesystem patches. RUN patch /usr/local/go/src/runtime/rt0_nacl_amd64p32.s /usr/local/playground/enable-fake-time.patch -RUN cd /usr/local/go && go run misc/nacl/mkzip.go -p syscall /usr/local/playground/fake_fs.lst src/syscall/fstest_nacl.go -# Re-build the Go toolchain. +RUN cd /usr/local/go && $GOROOT_BOOTSTRAP/bin/go run misc/nacl/mkzip.go -p syscall /usr/local/playground/fake_fs.lst src/syscall/fstest_nacl.go +# Build the Go toolchain. RUN cd /usr/local/go/src && GOOS=nacl GOARCH=amd64p32 ./make.bash --no-clean RUN mkdir /gocache diff --git a/cloudbuild_trigger.json b/cloudbuild_trigger.json index 2fa83048..d37d803a 100644 --- a/cloudbuild_trigger.json +++ b/cloudbuild_trigger.json @@ -19,16 +19,30 @@ "https://go.googlesource.com/playground" ] }, + { + "name": "golang", + "entrypoint": "bash", + "args": [ + "-c", + "go get golang.org/x/build/maintner/maintq && cp /go/bin/maintq /workspace/maintq" + ] + }, + { + "name": "debian:stretch", + "entrypoint": "bash", + "args": [ + "-c", + "apt-get update && apt-get install -y ca-certificates && ./maintq list-releases | head -n 1 | sed -re 's/^.*tag_name:\"(go[0-9](\\.[0-9]+)+)\".*$/\\1/g' > /workspace/latestgo" + ] + }, { "dir": "playground", "name": "gcr.io/cloud-builders/docker", + "entrypoint": "bash", "args": [ - "build", - "-t", - "gcr.io/$PROJECT_ID/playground", - "." - ], - "timeout": "1800s" + "-c", + "docker build --build-arg GO_VERSION=`cat /workspace/latestgo` -t gcr.io/$PROJECT_ID/playground ." + ] }, { "dir": "playground", @@ -39,10 +53,10 @@ "app.yaml", "--project=$PROJECT_ID", "--image-url=gcr.io/$PROJECT_ID/playground:latest" - ], - "timeout": "1200s" + ] } ], + "timeout": "1800s", "images": [ "gcr.io/$PROJECT_ID/playground" ] From 18ef8a7496c375dc3df2b965a7a9475060736c23 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Tue, 9 Jul 2019 14:24:17 -0400 Subject: [PATCH 027/148] playground: push image in trigger before deploying Removes post-build trigger step of pushing image, and adds a push step step before AppEngine deployment. This ensures that the image we just built is pushed before asking AppEngine to deploy it. This is documented at: https://cloud.google.com/cloud-build/docs/configuring-builds/build-test-deploy-artifacts Cloud Build is authorized to push images to gcr.io. This change was tested by submitting a build with this configuration without the deployment step. Updates golang/go#32606 Change-Id: I4e7cc242566378e4ca6d52244fb81bf410e79314 Reviewed-on: https://go-review.googlesource.com/c/playground/+/185339 Reviewed-by: Dmitri Shuralyov --- cloudbuild_trigger.json | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cloudbuild_trigger.json b/cloudbuild_trigger.json index d37d803a..620d320a 100644 --- a/cloudbuild_trigger.json +++ b/cloudbuild_trigger.json @@ -44,6 +44,13 @@ "docker build --build-arg GO_VERSION=`cat /workspace/latestgo` -t gcr.io/$PROJECT_ID/playground ." ] }, + { + "name": "gcr.io/cloud-builders/docker", + "args": [ + "push", + "gcr.io/$PROJECT_ID/playground" + ] + }, { "dir": "playground", "name": "gcr.io/cloud-builders/gcloud", @@ -56,9 +63,6 @@ ] } ], - "timeout": "1800s", - "images": [ - "gcr.io/$PROJECT_ID/playground" - ] + "timeout": "1800s" } } From 01ce4a0aa7eeec5f6bf0f7d3f071aad78d19bb4d Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Tue, 9 Jul 2019 16:30:07 -0400 Subject: [PATCH 028/148] playground: trigger playground deploys from master This change adds configuration to trigger a playground deploy when master is updated. Configuration for deploying a build with the latest release of go is shared in deploy/deploy.json. Cloud Build configurations have been moved to the deploy directory. In order to help share configurations, the build triggers will create a separate deployment build asynchronously. The README is updated to help describe common deployment scenarios. Tested deploy steps with README instructions. Updates golang/go#32606 Change-Id: I6eced05d3aa2904135e95e8b40e5a30a5b0b0488 Reviewed-on: https://go-review.googlesource.com/c/playground/+/185341 Reviewed-by: Dmitri Shuralyov --- Makefile | 13 +++++-- README.md | 36 ++++++++++++------ cloudbuild_trigger.json | 68 ---------------------------------- deploy/deploy.json | 46 +++++++++++++++++++++++ deploy/go_trigger.json | 36 ++++++++++++++++++ deploy/playground_trigger.json | 12 ++++++ 6 files changed, 128 insertions(+), 83 deletions(-) delete mode 100644 cloudbuild_trigger.json create mode 100644 deploy/deploy.json create mode 100644 deploy/go_trigger.json create mode 100644 deploy/playground_trigger.json diff --git a/Makefile b/Makefile index b0c86516..2cf568d5 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ -CLOUDBUILD_TRIGGER_ID := $(shell jq -r .id cloudbuild_trigger.json) -CLOUDBUILD_TRIGGER_JSON := cloudbuild_trigger.json +CLOUDBUILD_PLAYGROUND_TRIGGER_JSON := deploy/playground_trigger.json +CLOUDBUILD_PLAYGROUND_TRIGGER_ID := $(shell jq -r .id ${CLOUDBUILD_PLAYGROUND_TRIGGER_JSON}) +CLOUDBUILD_GO_TRIGGER_JSON := deploy/go_trigger.json +CLOUDBUILD_GO_TRIGGER_ID := $(shell jq -r .id ${CLOUDBUILD_GO_TRIGGER_JSON}) GCLOUD_ACCESS_TOKEN := $(shell gcloud auth print-access-token) .PHONY: docker test update-cloudbuild-trigger @@ -17,5 +19,8 @@ test: update-cloudbuild-trigger: # The gcloud CLI doesn't yet support updating a trigger. curl -H "Authorization: Bearer $(GCLOUD_ACCESS_TOKEN)" -H "Content-Type: application/json" \ - -d @$(CLOUDBUILD_TRIGGER_JSON) \ - -X PATCH https://cloudbuild.googleapis.com/v1/projects/golang-org/triggers/$(CLOUDBUILD_TRIGGER_ID) + -d @$(CLOUDBUILD_GO_TRIGGER_JSON) \ + -X PATCH https://cloudbuild.googleapis.com/v1/projects/golang-org/triggers/$(CLOUDBUILD_GO_TRIGGER_ID) + curl -H "Authorization: Bearer $(GCLOUD_ACCESS_TOKEN)" -H "Content-Type: application/json" \ + -d @$(CLOUDBUILD_PLAYGROUND_TRIGGER_JSON) \ + -X PATCH https://cloudbuild.googleapis.com/v1/projects/golang-org/triggers/$(CLOUDBUILD_PLAYGROUND_TRIGGER_ID) diff --git a/README.md b/README.md index fffee73f..6b8d71ca 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,31 @@ cat /path/to/code.go | go run client.go | curl -s --upload-file - localhost:8080 ## Deployment +### Deployment Triggers + +Playground releases automatically triggered when new Go repository tags are pushed to GitHub, or when master is pushed +on the playground repository. + +For details, see [deploy/go_trigger.json](deploy/go_trigger.json), +[deploy/playground_trigger.json](deploy/playground_trigger.json), +and [deploy/deploy.json](deploy/deploy.json). + +After making changes to trigger configuration, update configuration by running the following Make target: + +```bash +make update-cloudbuild-trigger +``` + +### Deploy via Cloud Build + +The Cloud Build configuration will always build and deploy with the latest supported release of Go. + +```bash +gcloud builds submit --config deploy/deploy.json . +``` + +### Deploy via gcloud app deploy + Building the playground Docker container takes more than the default 10 minute time limit of cloud build, so increase its timeout first (note, `app/cloud_build_timeout` is a global configuration value): @@ -42,17 +67,6 @@ Then: gcloud --project=golang-org --account=you@google.com app deploy app.yaml ``` -### Deployment Triggers - -Playground releases are also triggered when new tags are pushed to Github. The Cloud Build trigger configuration is -defined in [cloudbuild_trigger.json](cloudbuild_trigger.json). - -Triggers can be updated by running the following Make target: - -```bash -make update-cloudbuild-trigger -``` - ## Contributing To submit changes to this repository, see diff --git a/cloudbuild_trigger.json b/cloudbuild_trigger.json deleted file mode 100644 index 620d320a..00000000 --- a/cloudbuild_trigger.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "id": "5a2c9e25-a71a-4adf-a785-76c3eca2ac8a", - "description": "Go repository release trigger for x/playground", - "github": { - "name": "go", - "owner": "golang", - "push": { - "tag": "^go[0-9](\\.[0-9]+)+$" - } - }, - "build": { - "steps": [ - { - "name": "gcr.io/cloud-builders/git", - "args": [ - "clone", - "--depth", - "1", - "https://go.googlesource.com/playground" - ] - }, - { - "name": "golang", - "entrypoint": "bash", - "args": [ - "-c", - "go get golang.org/x/build/maintner/maintq && cp /go/bin/maintq /workspace/maintq" - ] - }, - { - "name": "debian:stretch", - "entrypoint": "bash", - "args": [ - "-c", - "apt-get update && apt-get install -y ca-certificates && ./maintq list-releases | head -n 1 | sed -re 's/^.*tag_name:\"(go[0-9](\\.[0-9]+)+)\".*$/\\1/g' > /workspace/latestgo" - ] - }, - { - "dir": "playground", - "name": "gcr.io/cloud-builders/docker", - "entrypoint": "bash", - "args": [ - "-c", - "docker build --build-arg GO_VERSION=`cat /workspace/latestgo` -t gcr.io/$PROJECT_ID/playground ." - ] - }, - { - "name": "gcr.io/cloud-builders/docker", - "args": [ - "push", - "gcr.io/$PROJECT_ID/playground" - ] - }, - { - "dir": "playground", - "name": "gcr.io/cloud-builders/gcloud", - "args": [ - "app", - "deploy", - "app.yaml", - "--project=$PROJECT_ID", - "--image-url=gcr.io/$PROJECT_ID/playground:latest" - ] - } - ], - "timeout": "1800s" - } -} diff --git a/deploy/deploy.json b/deploy/deploy.json new file mode 100644 index 00000000..72d9415b --- /dev/null +++ b/deploy/deploy.json @@ -0,0 +1,46 @@ +{ + "steps": [ + { + "name": "golang", + "entrypoint": "bash", + "args": [ + "-c", + "cd go && go get golang.org/x/build/maintner/maintq && cp /go/bin/maintq /workspace/maintq" + ] + }, + { + "name": "debian:stretch", + "entrypoint": "bash", + "args": [ + "-c", + "apt-get update && apt-get install -y ca-certificates && ./maintq list-releases | head -n 1 | sed -re 's/^.*tag_name:\"(go[0-9](\\.[0-9]+)+)\".*$/\\1/g' > /workspace/latestgo" + ] + }, + { + "name": "gcr.io/cloud-builders/docker", + "entrypoint": "bash", + "args": [ + "-c", + "docker build --build-arg GO_VERSION=`cat /workspace/latestgo` -t gcr.io/$PROJECT_ID/playground ." + ] + }, + { + "name": "gcr.io/cloud-builders/docker", + "args": [ + "push", + "gcr.io/$PROJECT_ID/playground" + ] + }, + { + "name": "gcr.io/cloud-builders/gcloud", + "args": [ + "app", + "deploy", + "app.yaml", + "--project=$PROJECT_ID", + "--image-url=gcr.io/$PROJECT_ID/playground:latest" + ] + } + ], + "timeout": "1800s" +} diff --git a/deploy/go_trigger.json b/deploy/go_trigger.json new file mode 100644 index 00000000..b7e7ff59 --- /dev/null +++ b/deploy/go_trigger.json @@ -0,0 +1,36 @@ +{ + "id": "5a2c9e25-a71a-4adf-a785-76c3eca2ac8a", + "description": "Go repository release trigger for x/playground", + "github": { + "name": "go", + "owner": "golang", + "push": { + "tag": "^go[0-9](\\.[0-9]+)+$" + } + }, + "build": { + "steps": [ + { + "name": "gcr.io/cloud-builders/git", + "args": [ + "clone", + "--depth", + "1", + "https://go.googlesource.com/playground" + ] + }, + { + "dir": "playground", + "name": "gcr.io/cloud-builders/gcloud", + "args": [ + "builds", + "submit", + "--async", + "--config", + "deploy/deploy.json", + "." + ] + } + ] + } +} diff --git a/deploy/playground_trigger.json b/deploy/playground_trigger.json new file mode 100644 index 00000000..3685fe51 --- /dev/null +++ b/deploy/playground_trigger.json @@ -0,0 +1,12 @@ +{ + "id": "cb46eb75-8665-4365-8e93-b8f7bfbd4807", + "description": "Playground repository release trigger for x/playground", + "github": { + "name": "playground", + "owner": "golang", + "push": { + "branch": "^master$" + } + }, + "filename": "deploy/deploy.json" +} From 5fdf6019ac9accf6dfeb5bb15b7a7e80cb6411c7 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Tue, 9 Jul 2019 18:30:52 -0400 Subject: [PATCH 029/148] playground: simplify latest go version logic This changes the build process to use a newly-added latestgo command for determining the latest version of Go. This simplifies the Cloud Build deploy.json significantly. latestgo fetches the latest supported Go releases from maintner, returning the first one. The maintner API always returns at least 2 releases on success, with the first result being the latest supported Go release. Updates golang/go#32606 Change-Id: I9da9101368ac4edfd7651e3f996b5f2dbe51b6c4 Reviewed-on: https://go-review.googlesource.com/c/playground/+/185437 Reviewed-by: Dmitri Shuralyov --- cmd/latestgo/main.go | 32 ++++++++++++++++++++++++++++++ deploy/deploy.json | 15 +++++---------- go.mod | 2 ++ go.sum | 46 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 cmd/latestgo/main.go diff --git a/cmd/latestgo/main.go b/cmd/latestgo/main.go new file mode 100644 index 00000000..6e8a6904 --- /dev/null +++ b/cmd/latestgo/main.go @@ -0,0 +1,32 @@ +// 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. + +// latestgo prints the latest Go release tag to stdout as a part of the playground deployment process. +package main + +import ( + "context" + "fmt" + "log" + + "golang.org/x/build/maintner/maintnerd/apipb" + grpc "grpc.go4.org" +) + +const maintnerURI = "https://maintner.golang.org" + +func main() { + conn, err := grpc.NewClient(nil, maintnerURI) + if err != nil { + log.Fatalf("error creating grpc client for %q: %v", maintnerURI, err) + } + mc := apipb.NewMaintnerServiceClient(conn) + + resp, err := mc.ListGoReleases(context.Background(), &apipb.ListGoReleasesRequest{}) + if err != nil { + log.Fatalln(err) + } + // On success, the maintner API always returns at least two releases. + fmt.Print(resp.GetReleases()[0].GetTagName()) +} diff --git a/deploy/deploy.json b/deploy/deploy.json index 72d9415b..83a0705a 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -2,18 +2,13 @@ "steps": [ { "name": "golang", + "env": [ + "GO111MODULE=on" + ], "entrypoint": "bash", "args": [ "-c", - "cd go && go get golang.org/x/build/maintner/maintq && cp /go/bin/maintq /workspace/maintq" - ] - }, - { - "name": "debian:stretch", - "entrypoint": "bash", - "args": [ - "-c", - "apt-get update && apt-get install -y ca-certificates && ./maintq list-releases | head -n 1 | sed -re 's/^.*tag_name:\"(go[0-9](\\.[0-9]+)+)\".*$/\\1/g' > /workspace/latestgo" + "go run golang.org/x/playground/cmd/latestgo > /workspace/goversion; echo GO_VERSION=`cat /workspace/goversion`" ] }, { @@ -21,7 +16,7 @@ "entrypoint": "bash", "args": [ "-c", - "docker build --build-arg GO_VERSION=`cat /workspace/latestgo` -t gcr.io/$PROJECT_ID/playground ." + "docker build --build-arg GO_VERSION=`cat /workspace/goversion` -t gcr.io/$PROJECT_ID/playground ." ] }, { diff --git a/go.mod b/go.mod index be07315e..70ce9b14 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( cloud.google.com/go v0.38.0 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 github.com/rogpeppe/go-internal v1.3.0 + golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 + grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 ) diff --git a/go.sum b/go.sum index 8a315b3b..d294af15 100644 --- a/go.sum +++ b/go.sum @@ -3,49 +3,78 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/playground v0.0.0-20190709222844-01ce4a0aa7ee h1:49sRs1OfqzbPBi4arOd89Wvpx3yjvtAzL4ZJKm1jwAU= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 h1:TJegdELkXYRfV3+4F6OEL777GyidAUcamDxkvQhIZCA= +golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0/go.mod h1:fYw7AShPAhGMdXqA9gRadk/CcMsvLlClpE5oBwnS3dM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -55,12 +84,20 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w= +golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 h1:zHwPzzQF2U6W4cSM2929cb7MvpB6dLYu9dHwYjOv+ag= golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -69,13 +106,22 @@ google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEt google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= From 509e10fc018f6da63e433d45afbf5c21af9480a2 Mon Sep 17 00:00:00 2001 From: Jack Lindamood Date: Mon, 1 Jul 2019 13:49:30 -0700 Subject: [PATCH 030/148] playground: allow explicit GOPROXY variable Setting the PLAY_GOPROXY variable of the playground modifies where the playground fetches module information from. This allows users to install the playground inside their company VPN and use private modules. The variable isn't named "GOPROXY" to not confuse which context the URL is used for. The name PLAY_* is adopted from the existing env variable ALLOW_PLAY_MODULE_DOWNLOADS. Fixes golang/go#32740 Change-Id: Idcd0530c1c28342414e0bc5439fca6a2272e1eaa Reviewed-on: https://go-review.googlesource.com/c/playground/+/184558 Reviewed-by: Andrew Bonventre --- sandbox.go | 15 +++++++++++++-- server_test.go | 31 +++++++++++++++++++++++++++++++ vet.go | 2 +- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/sandbox.go b/sandbox.go index c4a865d4..20508325 100644 --- a/sandbox.go +++ b/sandbox.go @@ -370,7 +370,7 @@ func compileAndRun(req *request) (*response, error) { return nil, fmt.Errorf("error creating temp directory: %v", err) } defer os.RemoveAll(goPath) - cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY=https://proxy.golang.org") + cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY="+playgroundGoproxy()) } else { goPath = os.Getenv("GOPATH") // contains old code.google.com/p/go-tour, etc cmd.Env = append(cmd.Env, "GO111MODULE=off") // in case it becomes on by default later @@ -451,13 +451,24 @@ func allowModuleDownloads(files *fileSet) bool { // these packages to still run, so the Dockerfile adds these packages // at this name in $GOPATH. Any snippets using this old name wouldn't // have expected (or been able to use) third-party packages anyway, - // so disabling modules and proxy.golang.org fetches is acceptable. + // so disabling modules and proxy fetches is acceptable. return false } v, _ := strconv.ParseBool(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) return v } +// playgroundGoproxy returns the GOPROXY environment config the playground should use. +// It is fetched from the environment variable PLAY_GOPROXY. A missing or empty +// value for PLAY_GOPROXY returns the default value of https://proxy.golang.org. +func playgroundGoproxy() string { + proxypath := os.Getenv("PLAY_GOPROXY") + if proxypath != "" { + return proxypath + } + return "https://proxy.golang.org" +} + func (s *server) healthCheck() error { resp, err := compileAndRun(&request{Body: healthProg}) if err != nil { diff --git a/server_test.go b/server_test.go index 9cc19da0..773a0934 100644 --- a/server_test.go +++ b/server_test.go @@ -272,5 +272,36 @@ func TestAllowModuleDownloads(t *testing.T) { t.Errorf("%d. allow = %v; want %v; files:\n%s", i, got, tt.want, filesAsString(files)) } } +} + +func TestPlaygroundGoproxy(t *testing.T) { + const envKey = "PLAY_GOPROXY" + defer os.Setenv(envKey, os.Getenv(envKey)) + tests := []struct { + name string + env string + want string + }{ + {name: "missing", env: "", want: "https://proxy.golang.org"}, + {name: "set_to_default", env: "https://proxy.golang.org", want: "https://proxy.golang.org"}, + {name: "changed", env: "https://company.intranet", want: "https://company.intranet"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.env != "" { + if err := os.Setenv(envKey, tt.env); err != nil { + t.Errorf("unable to set environment variable for test: %s", err) + } + } else { + if err := os.Unsetenv(envKey); err != nil { + t.Errorf("unable to unset environment variable for test: %s", err) + } + } + got := playgroundGoproxy() + if got != tt.want { + t.Errorf("playgroundGoproxy = %s; want %s; env: %s", got, tt.want, tt.env) + } + }) + } } diff --git a/vet.go b/vet.go index 32edeca0..a9cae12a 100644 --- a/vet.go +++ b/vet.go @@ -60,7 +60,7 @@ func vetCheckInDir(dir, goPath string, modules bool) (output string, execErr err if modules { cmd.Env = append(cmd.Env, "GO111MODULE=on", - "GOPROXY=https://proxy.golang.org", + "GOPROXY="+playgroundGoproxy(), ) } out, err := cmd.CombinedOutput() From ee0783da1b56fb41659f43e524908addf8233639 Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Tue, 30 Jul 2019 18:20:20 -0400 Subject: [PATCH 031/148] playground: set GOPROXY when building Docker image Change-Id: Ia59f12bb2ac6576409d10645885c8fd87ff528b1 Reviewed-on: https://go-review.googlesource.com/c/playground/+/188240 Reviewed-by: Alexander Rakoczy --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 7ddad2a7..09437716 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,7 @@ RUN cd /usr/local/go/src && GOOS=nacl GOARCH=amd64p32 ./make.bash --no-clean RUN mkdir /gocache ENV GOCACHE /gocache ENV GO111MODULE on +ENV GOPROXY=https://proxy.golang.org COPY go.mod /go/src/playground/go.mod COPY go.sum /go/src/playground/go.sum From a712dec9c06f6325a663f6c188d8b3a8646df8bc Mon Sep 17 00:00:00 2001 From: Andrew Bonventre Date: Mon, 2 Sep 2019 16:06:27 -0400 Subject: [PATCH 032/148] Dockerfile: update Go version to 1.13 Change-Id: I7a4dd7168e8ae8f4898de615dc5564f76114038f Reviewed-on: https://go-review.googlesource.com/c/playground/+/192917 Reviewed-by: Dmitri Shuralyov Reviewed-by: Alexander Rakoczy --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 09437716..2e84a6cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,8 +17,8 @@ RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap -ENV GO_BOOTSTRAP_VERSION go1.12.6 -ARG GO_VERSION=go1.12.6 +ENV GO_BOOTSTRAP_VERSION go1.13 +ARG GO_VERSION=go1.13 ENV GO_VERSION ${GO_VERSION} # Fake time From 394f19fbd839f9b55d55db2246a1cb97ac3f0fe2 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 16 May 2019 14:50:04 +0000 Subject: [PATCH 033/148] playground: differentiate cache key based on client JS version Don't share a cache namespace between clients wanting vet (new) and not wanting vet (old). This was causing issues like: https://github.com/golang/go/issues/31944#issuecomment-493094932 And unrelated: a README deploy fix, broken from my earlier Makefile cleanup. Change-Id: Ibed7f91c739ac65e33ee8d73eed066a7a81f938c Reviewed-on: https://go-review.googlesource.com/c/playground/+/177617 Reviewed-by: Emmanuel Odeke Reviewed-by: Andrew Bonventre --- README.md | 2 +- sandbox.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b8d71ca..fe6ac19c 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ Alternatively, to avoid Cloud Build and build locally: ```bash make docker -docker tag playground:latest gcr.io/golang-org/playground:latest +docker tag golang/playground:latest gcr.io/golang-org/playground:latest docker push gcr.io/golang-org/playground:latest gcloud --project=golang-org --account=you@google.com app deploy app.yaml --image-url=gcr.io/golang-org/playground:latest ``` diff --git a/sandbox.go b/sandbox.go index 20508325..f43eb07a 100644 --- a/sandbox.go +++ b/sandbox.go @@ -74,6 +74,7 @@ type response struct { // The handler returned supports Cross-Origin Resource Sharing (CORS) from any domain. func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*response, error)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + cachePrefix := cachePrefix // so we can modify it below w.Header().Set("Access-Control-Allow-Origin", "*") if r.Method == "OPTIONS" { // This is likely a pre-flight CORS request. @@ -92,6 +93,10 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*res return } + if req.WithVet { + cachePrefix += "_vet" // "prog" -> "prog_vet" + } + resp := &response{} key := cacheKey(cachePrefix, req.Body) if err := s.cache.Get(key, resp); err != nil { From bd57f61ee04d92c02e587c013a88668eaba8d5db Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 10 Sep 2019 20:54:26 +0000 Subject: [PATCH 034/148] playground: bound compilation time too We already limited execution time. Change-Id: I4cdf0178e08632ad25e4a7c5d6d63a9a00131e31 Reviewed-on: https://go-review.googlesource.com/c/playground/+/194601 Reviewed-by: Ian Lance Taylor Reviewed-by: Filippo Valsorda --- README.md | 2 +- sandbox.go | 23 +++++++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index fe6ac19c..5ee85f9a 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ docker build -t playground . ## Running ```bash -docker run --name=play --rm -d -p 8080:8080 playground +docker run --name=play --rm -p 8080:8080 golang/playground & # run some Go code cat /path/to/code.go | go run client.go | curl -s --upload-file - localhost:8080/compile ``` diff --git a/sandbox.go b/sandbox.go index f43eb07a..7b6909f7 100644 --- a/sandbox.go +++ b/sandbox.go @@ -34,16 +34,23 @@ import ( ) const ( - maxRunTime = 2 * time.Second + maxCompileTime = 5 * time.Second + maxRunTime = 2 * time.Second // progName is the implicit program name written to the temp // dir and used in compiler and vet errors. progName = "prog.go" ) +const goBuildTimeoutError = "timeout running go build" + // Responses that contain these strings will not be cached due to // their non-deterministic nature. -var nonCachingErrors = []string{"out of memory", "cannot allocate memory"} +var nonCachingErrors = []string{ + "out of memory", + "cannot allocate memory", + goBuildTimeoutError, +} type request struct { Body string @@ -363,7 +370,10 @@ func compileAndRun(req *request) (*response, error) { exe := filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") - cmd := exec.Command("go", "build", "-o", exe, buildPkgArg) + + ctx, cancel := context.WithTimeout(context.Background(), maxCompileTime) + defer cancel() + cmd := exec.CommandContext(ctx, "go", "build", "-o", exe, buildPkgArg) cmd.Dir = tmpDir var goPath string cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache} @@ -381,7 +391,12 @@ func compileAndRun(req *request) (*response, error) { cmd.Env = append(cmd.Env, "GO111MODULE=off") // in case it becomes on by default later } cmd.Env = append(cmd.Env, "GOPATH="+goPath) + t0 := time.Now() if out, err := cmd.CombinedOutput(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + log.Printf("go build timed out after %v", time.Since(t0)) + return &response{Errors: goBuildTimeoutError}, nil + } if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. @@ -396,7 +411,7 @@ func compileAndRun(req *request) (*response, error) { } return nil, fmt.Errorf("error building go source: %v", err) } - ctx, cancel := context.WithTimeout(context.Background(), maxRunTime) + ctx, cancel = context.WithTimeout(context.Background(), maxRunTime) defer cancel() cmd = exec.CommandContext(ctx, "sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", exe, testParam) rec := new(Recorder) From a46a9c27bdf208498a0ce100d3ab5c3aab749240 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 19 Nov 2019 23:06:14 +0000 Subject: [PATCH 035/148] playground: use txtar and modfile from golang.org/x txtar was promoted to no longer be internal in CL 207905 Change-Id: I3edfd84a53c04d097de85118d9a496aebb953ba2 Reviewed-on: https://go-review.googlesource.com/c/playground/+/207847 Reviewed-by: Jay Conrod --- fmt.go | 2 +- go.mod | 5 ++--- go.sum | 23 ++++++++++------------- txtar.go | 2 +- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/fmt.go b/fmt.go index 002bd34b..88c07466 100644 --- a/fmt.go +++ b/fmt.go @@ -11,7 +11,7 @@ import ( "net/http" "path" - "github.com/rogpeppe/go-internal/modfile" + "golang.org/x/mod/modfile" "golang.org/x/tools/imports" ) diff --git a/go.mod b/go.mod index 70ce9b14..039b6eaf 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,8 @@ go 1.12 require ( cloud.google.com/go v0.38.0 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 - github.com/rogpeppe/go-internal v1.3.0 golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 - golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect - golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 + golang.org/x/mod v0.1.1-0.20191119225628-919e395dadcd + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 ) diff --git a/go.sum b/go.sum index d294af15..cc443522 100644 --- a/go.sum +++ b/go.sum @@ -16,7 +16,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/playground v0.0.0-20190709222844-01ce4a0aa7ee h1:49sRs1OfqzbPBi4arOd89Wvpx3yjvtAzL4ZJKm1jwAU= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= @@ -39,13 +38,7 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -54,11 +47,14 @@ golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 h1:TJegdELkXYRfV3+4F6OEL77 golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0/go.mod h1:fYw7AShPAhGMdXqA9gRadk/CcMsvLlClpE5oBwnS3dM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.1.1-0.20191119225628-919e395dadcd h1:ijnuXbHkHl3nxLcqUTgY+LlU52Zl625Vo8PMDKx+Dyo= +golang.org/x/mod v0.1.1-0.20191119225628-919e395dadcd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -67,8 +63,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJV golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= -golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -99,8 +95,11 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73 h1:zHwPzzQF2U6W4cSM2929cb7MvpB6dLYu9dHwYjOv+ag= -golang.org/x/tools v0.0.0-20190513214131-2a413a02cc73/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -118,8 +117,6 @@ google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= diff --git a/txtar.go b/txtar.go index 9b699bf0..591dcbf7 100644 --- a/txtar.go +++ b/txtar.go @@ -11,7 +11,7 @@ import ( "path" "strings" - "github.com/rogpeppe/go-internal/txtar" + "golang.org/x/tools/txtar" ) // fileSet is a set of files. From 4d362417fd14b0b8349150cb28c3e8f2e756932e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 17 Sep 2019 20:05:10 +0000 Subject: [PATCH 036/148] sandbox: add gvisor runsc-based sandbox This creates a VM (running Container-Optimized OS) with configuration such that it boots up and downloads/configures the runsc Docker runtime, reloading the existing Docker daemon on the VM, and then creates a new privileged Docker container with the host's /var/run/docker.sock available to the container. From within that container it's then possible for the new sandbox HTTP server to create its own Docker containers running under gvisor (using docker run --runtime=runsc). This then adds a regional us-central1 load balancer and instance group manager & instane template to run these VMs automatically across us-central1. Then the play.golang.org frontend can hit that URL (http://sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal) Fixes golang/go#25224 Updates golang/go#30439 (remove nacl) Updates golang/go#33629 (this CL makes the playground support 2 versions) Change-Id: I56c8a86875abcde9d29fa7592b23c0ecd3861458 Reviewed-on: https://go-review.googlesource.com/c/playground/+/195983 Run-TryBot: Brad Fitzpatrick Reviewed-by: Alexander Rakoczy Reviewed-by: Emmanuel Odeke --- .gitignore | 1 + Dockerfile | 25 +- Makefile | 23 +- go.sum | 8 - main.go | 4 + sandbox.go | 122 ++++++-- sandbox/.gitignore | 1 + sandbox/Dockerfile | 34 +++ sandbox/Dockerfile.gvisor | 33 +++ sandbox/Makefile | 52 ++++ sandbox/cloud-init.yaml | 17 ++ sandbox/konlet.yaml | 16 ++ sandbox/sandbox.go | 521 ++++++++++++++++++++++++++++++++++ sandbox/sandbox.tf | 143 ++++++++++ sandbox/sandboxtypes/types.go | 22 ++ server_test.go | 2 +- tests.go | 97 ++++++- vet.go | 3 +- 18 files changed, 1066 insertions(+), 58 deletions(-) create mode 100644 .gitignore create mode 100644 sandbox/.gitignore create mode 100644 sandbox/Dockerfile create mode 100644 sandbox/Dockerfile.gvisor create mode 100644 sandbox/Makefile create mode 100644 sandbox/cloud-init.yaml create mode 100644 sandbox/konlet.yaml create mode 100644 sandbox/sandbox.go create mode 100644 sandbox/sandbox.tf create mode 100644 sandbox/sandboxtypes/types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3fa8c86b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.terraform diff --git a/Dockerfile b/Dockerfile index 2e84a6cf..063f9a52 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,12 +2,14 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. +############################################################################ FROM debian:stretch AS nacl RUN apt-get update && apt-get install -y --no-install-recommends curl bzip2 ca-certificates RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 +############################################################################ FROM debian:stretch AS build LABEL maintainer="golang-dev@googlegroups.com" @@ -64,12 +66,32 @@ COPY . /go/src/playground/ WORKDIR /go/src/playground RUN go install +############################################################################ +# Temporary Docker stage to add a pre-Go1.14 $GOROOT into our +# container for early linux/amd64 testing. +FROM golang:1.13 AS temp_pre_go14 + +ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' +RUN apt-get update && apt-get install -y --no-install-recommends ${BUILD_DEPS} + +# go1.14beta1: +ENV GO_REV a5bfd9da1d1b24f326399b6b75558ded14514f23 + +RUN cd /usr/local && git clone https://go.googlesource.com/go go1.14 && cd go1.14 && git reset --hard ${GO_REV} +WORKDIR /usr/local/go1.14/src +RUN ./make.bash +ENV GOROOT /usr/local/go1.14 +RUN ../bin/go install --tags=faketime std + +############################################################################ +# Final stage. FROM debian:stretch RUN apt-get update && apt-get install -y git ca-certificates --no-install-recommends COPY --from=build /usr/local/go /usr/local/go COPY --from=nacl /tmp/sel_ldr_x86_64 /usr/local/bin +COPY --from=temp_pre_go14 /usr/local/go1.14 /usr/local/go1.14 ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH @@ -101,9 +123,6 @@ COPY edit.html /app COPY static /app/static WORKDIR /app -# Run tests -RUN /app/playground test - # Whether we allow third-party imports via proxy.golang.org: ENV ALLOW_PLAY_MODULE_DOWNLOADS true diff --git a/Makefile b/Makefile index 2cf568d5..bac164f3 100644 --- a/Makefile +++ b/Makefile @@ -9,12 +9,27 @@ GCLOUD_ACCESS_TOKEN := $(shell gcloud auth print-access-token) docker: docker build -t golang/playground . -test: +runlocal: + docker network create sandnet || true + docker kill play_dev || true + docker run --name=play_dev --rm --network=sandnet -ti -p 127.0.0.1:8081:8080/tcp golang/playground + +test_go: # Run fast tests first: (and tests whether, say, things compile) GO111MODULE=on go test -v - # Then run the slower tests, which happen as one of the - # Dockerfile RUN steps: - docker build -t golang/playground . + +test_nacl: docker + docker kill sandbox_front_test || true + docker run --rm --name=sandbox_front_test --network=sandnet -t golang/playground testnacl + +test_gvisor: docker + docker kill sandbox_front_test || true + docker run --rm --name=sandbox_front_test --network=sandnet -t golang/playground test + +# Note: test_gvisor is not included in "test" yet, because it requires +# running a separate server first ("make runlocal" in the sandbox +# directory) +test: test_go test_nacl update-cloudbuild-trigger: # The gcloud CLI doesn't yet support updating a trigger. diff --git a/go.sum b/go.sum index cc443522..0c2bd239 100644 --- a/go.sum +++ b/go.sum @@ -59,14 +59,12 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -75,16 +73,13 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -103,17 +98,14 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= diff --git a/main.go b/main.go index 94a96bfb..3bba9aba 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,10 @@ func main() { s.test() return } + if len(os.Args) > 1 && os.Args[1] == "testnacl" { + s.testNacl() + return + } port := os.Getenv("PORT") if port == "" { diff --git a/sandbox.go b/sandbox.go index 7b6909f7..e559725b 100644 --- a/sandbox.go +++ b/sandbox.go @@ -12,6 +12,7 @@ import ( "context" "crypto/sha256" "encoding/json" + "errors" "fmt" "go/ast" "go/doc" @@ -26,11 +27,12 @@ import ( "runtime" "strconv" "strings" - "syscall" "text/template" "time" + "cloud.google.com/go/compute/metadata" "github.com/bradfitz/gomemcache/memcache" + "golang.org/x/playground/sandbox/sandboxtypes" ) const ( @@ -79,7 +81,7 @@ type response struct { // If there is no cached *response for the combination of cachePrefix and request.Body, // handler calls cmdFunc and in case of a nil error, stores the value of *response in the cache. // The handler returned supports Cross-Origin Resource Sharing (CORS) from any domain. -func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*response, error)) http.HandlerFunc { +func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context, *request) (*response, error)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { cachePrefix := cachePrefix // so we can modify it below w.Header().Set("Access-Control-Allow-Origin", "*") @@ -110,7 +112,7 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(*request) (*res if err != memcache.ErrCacheMiss { s.log.Errorf("s.cache.Get(%q, &response): %v", key, err) } - resp, err = cmdFunc(&req) + resp, err = cmdFunc(r.Context(), &req) if err != nil { s.log.Errorf("cmdFunc error: %v", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -315,7 +317,7 @@ var failedTestPattern = "--- FAIL" // The output of successfully ran program is returned in *response.Events. // If a program cannot be built or has timed out, // *response.Errors contains an explanation for a user. -func compileAndRun(req *request) (*response, error) { +func compileAndRun(ctx context.Context, req *request) (*response, error) { // TODO(andybons): Add semaphore to limit number of running programs at once. tmpDir, err := ioutil.TempDir("", "sandbox") if err != nil { @@ -368,15 +370,34 @@ func compileAndRun(req *request) (*response, error) { } } + // TODO: remove all this once Go 1.14 is out. This is a transitional/debug step + // to support both nacl & gvisor temporarily. + useGvisor := os.Getenv("GO_VERSION") >= "go1.14" || + os.Getenv("DEBUG_FORCE_GVISOR") == "1" || + strings.Contains(req.Body, "//play:gvisor\n") + exe := filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") - ctx, cancel := context.WithTimeout(context.Background(), maxCompileTime) + buildCtx, cancel := context.WithTimeout(ctx, maxCompileTime) defer cancel() - cmd := exec.CommandContext(ctx, "go", "build", "-o", exe, buildPkgArg) + goBin := "go" + if useGvisor { + goBin = "/usr/local/go1.14/bin/go" + } + cmd := exec.CommandContext(buildCtx, goBin, + "build", + "-o", exe, + "-tags=faketime", // required for Go 1.14+, no-op before + buildPkgArg) cmd.Dir = tmpDir var goPath string - cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32", "GOCACHE=" + goCache} + if useGvisor { + cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go1.14"} + } else { + cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32"} + } + cmd.Env = append(cmd.Env, "GOCACHE="+goCache) if useModules { // Create a GOPATH just for modules to be downloaded // into GOPATH/pkg/mod. @@ -411,28 +432,62 @@ func compileAndRun(req *request) (*response, error) { } return nil, fmt.Errorf("error building go source: %v", err) } - ctx, cancel = context.WithTimeout(context.Background(), maxRunTime) + runCtx, cancel := context.WithTimeout(ctx, maxRunTime) defer cancel() - cmd = exec.CommandContext(ctx, "sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", exe, testParam) rec := new(Recorder) - cmd.Stdout = rec.Stdout() - cmd.Stderr = rec.Stderr() - var status int - if err := cmd.Run(); err != nil { - if ctx.Err() == context.DeadlineExceeded { - // Send what was captured before the timeout. - events, err := rec.Events() - if err != nil { - return nil, fmt.Errorf("error decoding events: %v", err) - } - return &response{Errors: "process took too long", Events: events}, nil + var exitCode int + if useGvisor { + f, err := os.Open(exe) + if err != nil { + return nil, err } - exitErr, ok := err.(*exec.ExitError) - if !ok { - return nil, fmt.Errorf("error running sandbox: %v", err) + defer f.Close() + req, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), f) + if err != nil { + return nil, err + } + req.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST + if testParam != "" { + req.Header.Add("X-Argument", testParam) + } + req.GetBody = func() (io.ReadCloser, error) { return os.Open(exe) } + res, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err } - if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { - status = ws.ExitStatus() + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected response from backend: %v", res.Status) + } + var execRes sandboxtypes.Response + if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil { + log.Printf("JSON decode error from backend: %v", err) + return nil, errors.New("error parsing JSON from backend") + } + if execRes.Error != "" { + return &response{Errors: execRes.Error}, nil + } + exitCode = execRes.ExitCode + rec.Stdout().Write(execRes.Stdout) + rec.Stderr().Write(execRes.Stderr) + } else { + cmd := exec.CommandContext(runCtx, "sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", exe, testParam) + cmd.Stdout = rec.Stdout() + cmd.Stderr = rec.Stderr() + if err := cmd.Run(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + // Send what was captured before the timeout. + events, err := rec.Events() + if err != nil { + return nil, fmt.Errorf("error decoding events: %v", err) + } + return &response{Errors: "process took too long", Events: events}, nil + } + exitErr, ok := err.(*exec.ExitError) + if !ok { + return nil, fmt.Errorf("error running sandbox: %v", err) + } + exitCode = exitErr.ExitCode() } } events, err := rec.Events() @@ -455,7 +510,7 @@ func compileAndRun(req *request) (*response, error) { } return &response{ Events: events, - Status: status, + Status: exitCode, IsTest: testParam != "", TestsFailed: fails, VetErrors: vetOut, @@ -490,7 +545,8 @@ func playgroundGoproxy() string { } func (s *server) healthCheck() error { - resp, err := compileAndRun(&request{Body: healthProg}) + ctx := context.Background() // TODO: cap it to some reasonable timeout + resp, err := compileAndRun(ctx, &request{Body: healthProg}) if err != nil { return err } @@ -503,6 +559,18 @@ func (s *server) healthCheck() error { return nil } +func sandboxBackendURL() string { + if v := os.Getenv("SANDBOX_BACKEND_URL"); v != "" { + return v + } + id, _ := metadata.ProjectID() + switch id { + case "golang-org": + return "http://sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal/run" + } + panic(fmt.Sprintf("no SANDBOX_BACKEND_URL environment and no default defined for project %q", id)) +} + const healthProg = ` package main diff --git a/sandbox/.gitignore b/sandbox/.gitignore new file mode 100644 index 00000000..0f2bb690 --- /dev/null +++ b/sandbox/.gitignore @@ -0,0 +1 @@ +*.yaml.expanded diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile new file mode 100644 index 00000000..ceb5401a --- /dev/null +++ b/sandbox/Dockerfile @@ -0,0 +1,34 @@ +# This is the sandbox backend server. +# +# When it's run, the host maps in /var/run/docker.sock to this +# environment so the play-sandbox server can connect to the host's +# docker daemon, which has the gvisor "runsc" runtime available. + +FROM golang:1.13 AS build + +COPY . /go/src/playground +WORKDIR /go/src/playground/sandbox +RUN go install + +FROM debian:buster + +RUN apt-get update + +# Extra stuff for occasional debugging: +RUN apt-get install --yes strace lsof emacs25-nox net-tools tcpdump procps + +# Install Docker CLI: +RUN apt-get install --yes \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg2 \ + software-properties-common +RUN bash -c "curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -" +RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian buster stable" +RUN apt-get update +RUN apt-get install --yes docker-ce-cli + +COPY --from=build /go/bin/sandbox /usr/local/bin/play-sandbox + +ENTRYPOINT ["/usr/local/bin/play-sandbox"] diff --git a/sandbox/Dockerfile.gvisor b/sandbox/Dockerfile.gvisor new file mode 100644 index 00000000..42188871 --- /dev/null +++ b/sandbox/Dockerfile.gvisor @@ -0,0 +1,33 @@ +# This is the environment that the untrusted playground programs run within +# under gvisor. + +############################################################################ +# Import the sandbox server's container (which is assumed to be +# already built, as enforced by the Makefile), just so we can copy its +# binary out of it. The same binary is used as both as the server and the +# gvisor-contained helper. +FROM golang/playground-sandbox AS server + +############################################################################ +# Temporary nacl compatibility for development & incremental +# deployment purposes, so we can run the new server architecture in +# nacl mode for a bit, then opt-in linux/amd64 gvisor mode, and +# then once Go 1.14 is out for real we remove the nacl option and +# delete all the nacl code. +FROM debian:buster AS nacl +RUN apt-get update && apt-get install -y --no-install-recommends curl bzip2 ca-certificates +RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 + + +############################################################################ +# This is the actual environment things run in: a minimal busybox with glibc +# binaries so we can use cgo. +FROM busybox:glibc + +COPY --from=server /usr/local/bin/play-sandbox /usr/local/bin/play-sandbox + +# And this, temporarily, for being able to test the old nacl binaries +# with the new sandbox: +COPY --from=nacl /tmp/sel_ldr_x86_64 /usr/local/bin + +ENTRYPOINT ["/usr/local/bin/play-sandbox"] diff --git a/sandbox/Makefile b/sandbox/Makefile new file mode 100644 index 00000000..06c74964 --- /dev/null +++ b/sandbox/Makefile @@ -0,0 +1,52 @@ +ZONE := us-central1-f +TEST_VM := gvisor-cos-test-vm +PROJ := golang-org +NETWORK := golang + +# Docker environment for the sandbox server itself (containing docker CLI, etc), running +# in a privileged container. +docker: + docker build -f Dockerfile --tag=golang/playground-sandbox .. + docker tag golang/playground-sandbox gcr.io/$(PROJ)/playground-sandbox:latest + +# dockergvisor builds the golang/playground-sandbox-gvisor docker +# image, which is the environment that the untrusted programs run in +# (a busybox:glibc world with this directory's sandbox binary which +# runs in --mode=contained) +dockergvisor: + docker build -f Dockerfile.gvisor --tag=golang/playground-sandbox-gvisor .. + docker tag golang/playground-sandbox-gvisor gcr.io/$(PROJ)/playground-sandbox-gvisor:latest + +push: docker dockergvisor + docker push gcr.io/$(PROJ)/playground-sandbox:latest + docker push gcr.io/$(PROJ)/playground-sandbox-gvisor:latest + +# runlocal runs the sandbox server locally, for use with the frontend +# parent directory's "test_nacl" or "test_gvisor" test targets. +runlocal: docker dockergvisor + docker network create sandnet || true + docker kill sandbox_dev || true + docker run --name=sandbox_dev --rm --network=sandnet -ti -p 127.0.0.1:8080:80/tcp -v /var/run/docker.sock:/var/run/docker.sock golang/playground-sandbox:latest --dev + +konlet.yaml.expanded: konlet.yaml + sed "s/PROJECT_NAME/$(PROJ)/" konlet.yaml > konlet.yaml.expanded + +# create_test_vm creates a test VM for interactive debugging. +create_test_vm: konlet.yaml.expanded + gcloud --project=$(PROJ) compute instances create $(TEST_VM) \ + --zone $(ZONE) \ + --network $(NETWORK) \ + --no-address \ + --image-project cos-cloud \ + --image cos-stable-76-12239-60-0 \ + --metadata-from-file gce-container-declaration=konlet.yaml.expanded,user-data=cloud-init.yaml + +# delete_test_vm deletes the test VM from create_test_vm. +delete_test_vm: + gcloud --project=$(PROJ) compute instances delete $(TEST_VM) --quiet --zone $(ZONE) + +# ssh connects to the create_test_vm VM. It must be run from the same network. +ssh: + gcloud --project=$(PROJ) compute ssh $(TEST_VM) --internal-ip --zone $(ZONE) + + diff --git a/sandbox/cloud-init.yaml b/sandbox/cloud-init.yaml new file mode 100644 index 00000000..41195a4e --- /dev/null +++ b/sandbox/cloud-init.yaml @@ -0,0 +1,17 @@ +#cloud-config + +write_files: +- path: /etc/docker/daemon.json + permissions: 0644 + owner: root + content: | + { + "live-restore": true, + "storage-driver": "overlay2", + "runtimes": { "runsc": { "path": "/var/lib/docker/runsc", "runtimeArgs": [] } } + } + +runcmd: +- curl -L -o /var/lib/docker/runsc https://storage.googleapis.com/go-builder-data/runsc-1.0.1 +- chmod +x /var/lib/docker/runsc +- systemctl reload docker.service diff --git a/sandbox/konlet.yaml b/sandbox/konlet.yaml new file mode 100644 index 00000000..2850af34 --- /dev/null +++ b/sandbox/konlet.yaml @@ -0,0 +1,16 @@ +spec: + containers: + - name: playground + image: 'gcr.io/PROJECT_NAME/gvisor-playground-sandbox:latest' + volumeMounts: + - name: dockersock + mountPath: /var/run/docker.sock + securityContext: + privileged: true + stdin: false + tty: true + restartPolicy: Always + volumes: + - name: dockersock + hostPath: + path: /var/run/docker.sock diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go new file mode 100644 index 00000000..be7ab6d5 --- /dev/null +++ b/sandbox/sandbox.go @@ -0,0 +1,521 @@ +// 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 sandbox program is an HTTP server that receives untrusted +// linux/amd64 in a POST request and then executes them in a gvisor +// sandbox using Docker, returning the output as a response to the +// POST. +// +// It's part of the Go playground (https://play.golang.org/). +package main + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "os/exec" + "os/signal" + "runtime" + "sync" + "syscall" + "time" + + "golang.org/x/playground/sandbox/sandboxtypes" +) + +var ( + listenAddr = flag.String("listen", ":80", "HTTP server listen address. Only applicable when --mode=server") + mode = flag.String("mode", "server", "Whether to run in \"server\" mode or \"contained\" mode. The contained mode is used internally by the server mode.") + dev = flag.Bool("dev", false, "run in dev mode (show help messages)") + numWorkers = flag.Int("workers", runtime.NumCPU(), "number of parallel gvisor containers to pre-spin up & let run concurrently") +) + +const ( + maxBinarySize = 100 << 20 + runTimeout = 5 * time.Second + maxOutputSize = 100 << 20 + memoryLimitBytes = 100 << 20 +) + +var errTooMuchOutput = errors.New("Output too large") + +// containedStartMessage is the first thing written to stdout by the +// contained process when it starts up. This lets the parent HTTP +// server know that a particular container is ready to run a binary. +const containedStartMessage = "started\n" + +var ( + readyContainer chan *Container + runSem chan struct{} +) + +type Container struct { + name string + stdin io.WriteCloser + stdout io.ReadCloser + stderr io.ReadCloser + cmd *exec.Cmd + + waitOnce sync.Once + waitVal error +} + +func (c *Container) Close() { + setContainerWanted(c.name, false) + c.stdin.Close() + c.stdout.Close() + c.stderr.Close() + if c.cmd.Process != nil { + c.cmd.Process.Kill() + c.Wait() // just in case + } +} + +func (c *Container) Wait() error { + c.waitOnce.Do(c.wait) + return c.waitVal +} + +func (c *Container) wait() { + c.waitVal = c.cmd.Wait() +} + +func main() { + flag.Parse() + if *mode == "contained" { + runInGvisor() + panic("runInGvisor didn't exit") + } + if flag.NArg() != 0 { + flag.Usage() + os.Exit(1) + } + log.Printf("Go playground sandbox starting.") + + readyContainer = make(chan *Container, *numWorkers) + runSem = make(chan struct{}, *numWorkers) + go makeWorkers() + go handleSignals() + + if out, err := exec.Command("docker", "version").CombinedOutput(); err != nil { + log.Fatalf("failed to connect to docker: %v, %s", err, out) + } + if *dev { + log.Printf("Running in dev mode; container published to host at: http://localhost:8080/") + // TODO: XXXX FIXME: this is no longer the protocol since the addition of the processMeta JSON header, + // so write a client program to do this instead? + log.Printf("Run a binary with: curl -v --data-binary @/home/bradfitz/hello http://localhost:8080/run\n") + } else { + log.Printf("Listening on %s", *listenAddr) + } + + http.HandleFunc("/health", healthHandler) + http.HandleFunc("/healthz", healthHandler) + http.HandleFunc("/", rootHandler) + http.HandleFunc("/run", runHandler) + log.Fatal(http.ListenAndServe(*listenAddr, nil)) +} + +func handleSignals() { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGINT) + s := <-c + log.Fatalf("closing on signal %d: %v", s, s) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "OK\n") +} + +func rootHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/" { + http.NotFound(w, r) + return + } + io.WriteString(w, "Hi from sandbox\n") +} + +// processMeta is the JSON sent to the gvisor container before the untrusted binary. +// It currently contains only the arguments to pass to the binary. +// It might contain environment or other things later. +type processMeta struct { + Args []string `json:"args"` +} + +// runInGvisor is run when we're now inside gvisor. We have no network +// at this point. We can read our binary in from stdin and then run +// it. +func runInGvisor() { + const binPath = "/tmpfs/play" + if _, err := io.WriteString(os.Stdout, containedStartMessage); err != nil { + log.Fatalf("writing to stdout: %v", err) + } + slurp, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Fatalf("reading stdin in contained mode: %v", err) + } + nl := bytes.IndexByte(slurp, '\n') + if nl == -1 { + log.Fatalf("no newline found in input") + } + metaJSON, bin := slurp[:nl], slurp[nl+1:] + + if err := ioutil.WriteFile(binPath, bin, 0755); err != nil { + log.Fatalf("writing contained binary: %v", err) + } + + var meta processMeta + if err := json.NewDecoder(bytes.NewReader(metaJSON)).Decode(&meta); err != nil { + log.Fatalf("error decoding JSON meta: %v", err) + } + + // As part of a temporary transition plan, we also support + // running nacl binaries in this sandbox. The point isn't to + // double sandbox things as much as it is to let us transition + // things in steps: first to split the sandbox into two parts + // (frontend & backend), and then to change the type of binary + // (nacl to linux/amd64). This means we can do step 1 of the + // migration during the Go 1.13 dev cycle and have less + // risk/rush during the Go 1.14 release, which should just be + // a flag flip. + // This isn't a perfect heuristic, but it works and it's cheap: + isNacl := bytes.Contains(slurp, []byte("_rt0_amd64p32_nacl")) + + cmd := exec.Command(binPath) + if isNacl { + cmd = exec.Command("/usr/local/bin/sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", binPath) + } + cmd.Args = append(cmd.Args, meta.Args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Start(); err != nil { + log.Fatal(err) + } + err = cmd.Wait() + os.Remove(binPath) // not that it matters much, this container will be nuked + os.Exit(errExitCode(err)) + return + +} + +func makeWorkers() { + for { + c, err := startContainer(context.Background()) + if err != nil { + log.Printf("error starting container: %v", err) + time.Sleep(5 * time.Second) + continue + } + readyContainer <- c + } +} + +func randHex(n int) string { + b := make([]byte, n/2) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return fmt.Sprintf("%x", b) +} + +var ( + wantedMu sync.Mutex + containerWanted = map[string]bool{} +) + +// setContainerWanted records whether a named container is wanted or +// not. Any unwanted containers are cleaned up asynchronously as a +// sanity check against leaks. +// +// TODO(bradfitz): add leak checker (background docker ps loop) +func setContainerWanted(name string, wanted bool) { + wantedMu.Lock() + defer wantedMu.Unlock() + if wanted { + containerWanted[name] = true + } else { + delete(containerWanted, name) + } +} + +func getContainer(ctx context.Context) (*Container, error) { + select { + case c := <-readyContainer: + return c, nil + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func startContainer(ctx context.Context) (c *Container, err error) { + name := "play_run_" + randHex(8) + setContainerWanted(name, true) + var stdin io.WriteCloser + var stdout io.ReadCloser + var stderr io.ReadCloser + defer func() { + if err == nil { + return + } + setContainerWanted(name, false) + if stdin != nil { + stdin.Close() + } + if stdout != nil { + stdout.Close() + } + if stderr != nil { + stderr.Close() + } + }() + + cmd := exec.Command("docker", "run", + "--name="+name, + "--rm", + "--tmpfs=/tmpfs", + "-i", // read stdin + + "--runtime=runsc", + "--network=none", + "--memory="+fmt.Sprint(memoryLimitBytes), + + "gcr.io/golang-org/playground-sandbox-gvisor:latest", + "--mode=contained") + stdin, err = cmd.StdinPipe() + if err != nil { + return nil, err + } + stdout, err = cmd.StdoutPipe() + if err != nil { + return nil, err + } + stderr, err = cmd.StderrPipe() + if err != nil { + return nil, err + } + if err := cmd.Start(); err != nil { + return nil, err + } + + errc := make(chan error, 1) + go func() { + buf := make([]byte, len(containedStartMessage)) + if _, err := io.ReadFull(stdout, buf); err != nil { + errc <- fmt.Errorf("error reading header from sandbox container: %v", err) + return + } + if string(buf) != containedStartMessage { + errc <- fmt.Errorf("sandbox container sent wrong header %q; want %q", buf, containedStartMessage) + return + } + errc <- nil + }() + select { + case <-ctx.Done(): + log.Printf("timeout starting container") + cmd.Process.Kill() + return nil, ctx.Err() + case err := <-errc: + if err != nil { + log.Printf("error starting container: %v", err) + return nil, err + } + } + return &Container{ + name: name, + stdin: stdin, + stdout: stdout, + stderr: stderr, + cmd: cmd, + }, nil +} + +func runHandler(w http.ResponseWriter, r *http.Request) { + t0 := time.Now() + tlast := t0 + var logmu sync.Mutex + logf := func(format string, args ...interface{}) { + if !*dev { + return + } + logmu.Lock() + defer logmu.Unlock() + t := time.Now() + d := t.Sub(tlast) + d0 := t.Sub(t0) + tlast = t + log.Print(fmt.Sprintf("+%10v +%10v ", d0, d) + fmt.Sprintf(format, args...)) + } + logf("/run") + + if r.Method != "POST" { + http.Error(w, "expected a POST", http.StatusBadRequest) + return + } + + // Bound the number of requests being processed at once. + // (Before we slurp the binary into memory) + select { + case runSem <- struct{}{}: + case <-r.Context().Done(): + return + } + defer func() { <-runSem }() + + bin, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, maxBinarySize)) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + logf("read %d bytes", len(bin)) + + c, err := getContainer(r.Context()) + if err != nil { + if cerr := r.Context().Err(); cerr != nil { + log.Printf("getContainer, client side cancellation: %v", cerr) + return + } + http.Error(w, "failed to get container", http.StatusInternalServerError) + log.Printf("failed to get container: %v", err) + return + } + logf("got container %s", c.name) + defer c.Close() + defer logf("leaving handler; about to close container") + + runTimer := time.NewTimer(runTimeout) + defer runTimer.Stop() + + errc := make(chan error, 2) // user-visible error + waitc := make(chan error, 1) + + copyOut := func(which string, dst *[]byte, r io.Reader) { + buf := make([]byte, 4<<10) + for { + n, err := r.Read(buf) + logf("%s: Read = %v, %v", which, n, err) + *dst = append(*dst, buf[:n]...) + if err == io.EOF { + return + } + if len(*dst) > maxOutputSize { + errc <- errTooMuchOutput + return + } + if err != nil { + log.Printf("reading %s: %v", which, err) + errc <- fmt.Errorf("error reading %v", which) + return + } + } + } + + res := &sandboxtypes.Response{} + go func() { + var meta processMeta + meta.Args = r.Header["X-Argument"] + metaJSON, _ := json.Marshal(&meta) + metaJSON = append(metaJSON, '\n') + if _, err := c.stdin.Write(metaJSON); err != nil { + log.Printf("stdin write meta: %v", err) + errc <- errors.New("failed to write meta to child") + return + } + if _, err := c.stdin.Write(bin); err != nil { + log.Printf("stdin write: %v", err) + errc <- errors.New("failed to write binary to child") + return + } + c.stdin.Close() + logf("wrote+closed") + go copyOut("stdout", &res.Stdout, c.stdout) + go copyOut("stderr", &res.Stderr, c.stderr) + waitc <- c.Wait() + }() + var waitErr error + select { + case waitErr = <-waitc: + logf("waited: %v", waitErr) + case err := <-errc: + logf("got error: %v", err) + if err == errTooMuchOutput { + sendError(w, err.Error()) + return + } + if err != nil { + http.Error(w, "failed to read stdout from docker run", http.StatusInternalServerError) + return + } + case <-runTimer.C: + logf("timeout") + sendError(w, "timeout running program") + return + } + + res.ExitCode = errExitCode(waitErr) + res.Stderr = cleanStderr(res.Stderr) + sendResponse(w, res) +} + +func errExitCode(err error) int { + if err == nil { + return 0 + } + if ee, ok := err.(*exec.ExitError); ok { + return ee.ExitCode() + } + return 1 +} + +func sendError(w http.ResponseWriter, errMsg string) { + sendResponse(w, &sandboxtypes.Response{Error: errMsg}) +} + +func sendResponse(w http.ResponseWriter, r *sandboxtypes.Response) { + jres, err := json.MarshalIndent(r, "", " ") + if err != nil { + http.Error(w, "error encoding JSON", http.StatusInternalServerError) + log.Printf("json marshal: %v", err) + return + } + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", fmt.Sprint(len(jres))) + w.Write(jres) +} + +// cleanStderr removes spam stderr lines from the beginning of x +// and returns a slice of x. +func cleanStderr(x []byte) []byte { + for { + nl := bytes.IndexByte(x, '\n') + if nl == -1 || !isSpamStderrLine(x[:nl+1]) { + return x + } + x = x[nl+1:] + } +} + +var warningPrefix = []byte("WARNING: ") + +// isSpamStderrLine reports whether line is a spammy line of stderr +// output from Docker. Currently it only matches things starting with +// "WARNING: " like: +// WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. +// +// TODO: remove this and instead just make the child process start by +// writing a known header to stderr, then have parent skip everything +// before that unique header. +func isSpamStderrLine(line []byte) bool { + return bytes.HasPrefix(line, warningPrefix) +} diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf new file mode 100644 index 00000000..61029c12 --- /dev/null +++ b/sandbox/sandbox.tf @@ -0,0 +1,143 @@ +# TODO: move the network configuration into terraform too? It was created by hand with: +# gcloud compute networks subnets update golang --region=us-central1 --enable-private-ip-google-access + +terraform { + backend "gcs" { + bucket = "tf-state-prod-golang-org" + prefix = "terraform/state" + } +} + +provider "google-beta" { + project = "golang-org" + region = "us-central1" + zone = "us-central1-f" +} + +provider "google" { + project = "golang-org" + region = "us-central1" + zone = "us-central1-f" +} + +data "local_file" "cloud_init" { + filename = "${path.module}/cloud-init.yaml" +} + +data "local_file" "konlet" { + filename = "${path.module}/konlet.yaml.expanded" +} + +data "google_compute_image" "cos" { + family = "cos-stable" + project = "cos-cloud" +} + +resource "google_compute_instance_template" "inst_tmpl" { + name = "play-sandbox-tmpl" + machine_type = "n1-standard-1" + metadata = { + "ssh-keys" = "bradfitz:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtL4Di+zypBIRmvohzfC4mOzETaz/DtzANPKir/mUE1QiC8HuL8BDpIu3rZZY1gAnQf3IZMpgQYgx90TZskgWfi7xLN2mDh2iBZB0KUDlpNpwn1SzSUTQU10XV5mOLm1B7L+w3QXA1wz7kMjmztxZNli/tvZI5BtAX6c58c4Rn0pjQTPeEVPMMvEj6zPUy4QInWTgtrbTj3On/e95F3ZmUjEKFfjeFxVPh7fF6mSygzcxEyGYxTbMdUod/dNbZD/7HY5eTNQPSvjp+GaoodbFEdYWnk9vtoX/2VWYo0J/+w6oHprDzj7dPzKeJeCjpnKra7DyCS/RxIIPt5Giwdj2F bradfitz@bradfitz-glinux-desktop.sea.corp.google.com" + "gce-container-declaration" = data.local_file.konlet.content + "user-data" = data.local_file.cloud_init.content + } + network_interface { + network = "golang" + } + service_account { + scopes = ["storage-ro"] + } + disk { + source_image = data.google_compute_image.cos.self_link + auto_delete = true + boot = true + } + scheduling { + automatic_restart = true + on_host_maintenance = "MIGRATE" + } + lifecycle { + create_before_destroy = true + } +} + +resource "google_compute_region_autoscaler" "default" { + provider = "google-beta" + + name = "play-sandbox-autoscaler" + region = "us-central1" + target = "${google_compute_region_instance_group_manager.rigm.self_link}" + + autoscaling_policy { + max_replicas = 10 + min_replicas = 2 + cooldown_period = 60 + + cpu_utilization { + target = 0.5 + } + } +} + +resource "google_compute_region_instance_group_manager" "rigm" { + provider = "google-beta" + name = "play-sandbox-rigm" + + base_instance_name = "playsandbox" + region = "us-central1" + + version { + name = "primary" + instance_template = "${google_compute_instance_template.inst_tmpl.self_link}" + } + + named_port { + name = "http" + port = 80 + } + + auto_healing_policies { + health_check = "${google_compute_health_check.default.self_link}" + initial_delay_sec = 30 + } +} + +data "google_compute_region_instance_group" "rig" { + provider = "google-beta" + self_link = "${google_compute_region_instance_group_manager.rigm.instance_group}" +} + +resource "google_compute_health_check" "default" { + name = "play-sandbox-rigm-health-check" + check_interval_sec = 5 + timeout_sec = 5 + healthy_threshold = 2 + unhealthy_threshold = 10 # 50 seconds + http_health_check { + request_path = "/healthz" + port = 80 + } +} + +resource "google_compute_region_backend_service" "default" { + name = "play-sandbox-backend-service" + region = "us-central1" + health_checks = ["${google_compute_health_check.default.self_link}"] + backend { + group = "${data.google_compute_region_instance_group.rig.self_link}" + } +} + +resource "google_compute_forwarding_rule" "default" { + name = "play-sandbox-fwd" + region = "us-central1" + network = "golang" + ports = ["80"] + load_balancing_scheme = "INTERNAL" + ip_protocol = "TCP" + backend_service = "${google_compute_region_backend_service.default.self_link}" + + # Adding a service label gives us a DNS name: + # sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal + service_label = "sandbox" +} diff --git a/sandbox/sandboxtypes/types.go b/sandbox/sandboxtypes/types.go new file mode 100644 index 00000000..3ebfb46e --- /dev/null +++ b/sandbox/sandboxtypes/types.go @@ -0,0 +1,22 @@ +// 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 sandboxtypes package contains the shared types +// to communicate between the different sandbox components. +package sandboxtypes + +// Response is the response from the x/playground/sandbox backend to +// the x/playground frontend. +// +// The stdout/stderr are base64 encoded which isn't ideal but is good +// enough for now. Maybe we'll move to protobufs later. +type Response struct { + // Error, if non-empty, means we failed to run the binary. + // It's meant to be user-visible. + Error string `json:"error,omitempty"` + + ExitCode int `json:"exitCode"` + Stdout []byte `json:"stdout"` + Stderr []byte `json:"stderr"` +} diff --git a/server_test.go b/server_test.go index 773a0934..40ba0654 100644 --- a/server_test.go +++ b/server_test.go @@ -161,7 +161,7 @@ func TestCommandHandler(t *testing.T) { if err != nil { t.Fatalf("newServer(testingOptions(t)): %v", err) } - testHandler := s.commandHandler("test", func(r *request) (*response, error) { + testHandler := s.commandHandler("test", func(_ context.Context, r *request) (*response, error) { if r.Body == "fail" { return nil, fmt.Errorf("non recoverable") } diff --git a/tests.go b/tests.go index 18f6ea0a..4022b6a3 100644 --- a/tests.go +++ b/tests.go @@ -8,8 +8,10 @@ package main import ( + "context" "fmt" stdlog "log" + "net" "os" "reflect" "strings" @@ -19,12 +21,27 @@ import ( type compileTest struct { name string // test name prog, want, errors string + wantFunc func(got string) error // alternative to want withVet bool wantEvents []Event wantVetErrors string } +func (s *server) testNacl() { + log.Printf("testing nacl mode") + s.runTests() +} + func (s *server) test() { + if _, err := net.ResolveIPAddr("ip", "sandbox_dev.sandnet."); err != nil { + log.Fatalf("sandbox_dev.sandnet not available") + } + os.Setenv("DEBUG_FORCE_GVISOR", "1") + os.Setenv("SANDBOX_BACKEND_URL", "http://sandbox_dev.sandnet/run") + s.runTests() +} + +func (s *server) runTests() { if err := s.healthCheck(); err != nil { stdlog.Fatal(err) } @@ -33,44 +50,66 @@ func (s *server) test() { defer func(old string) { os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", old) }(os.Getenv("ALLOW_PLAY_MODULE_DOWNLOADS")) os.Setenv("ALLOW_PLAY_MODULE_DOWNLOADS", "true") + failed := false for i, t := range tests { - fmt.Printf("testing case %d (%q)...\n", i, t.name) - resp, err := compileAndRun(&request{Body: t.prog, WithVet: t.withVet}) + stdlog.Printf("testing case %d (%q)...\n", i, t.name) + resp, err := compileAndRun(context.Background(), &request{Body: t.prog, WithVet: t.withVet}) if err != nil { stdlog.Fatal(err) } if t.wantEvents != nil { if !reflect.DeepEqual(resp.Events, t.wantEvents) { - stdlog.Fatalf("resp.Events = %q, want %q", resp.Events, t.wantEvents) + stdlog.Printf("resp.Events = %q, want %q", resp.Events, t.wantEvents) + failed = true } continue } if t.errors != "" { if resp.Errors != t.errors { - stdlog.Fatalf("resp.Errors = %q, want %q", resp.Errors, t.errors) + stdlog.Printf("resp.Errors = %q, want %q", resp.Errors, t.errors) + failed = true } continue } if resp.Errors != "" { - stdlog.Fatal(resp.Errors) + stdlog.Print(resp.Errors) + failed = true + continue } if resp.VetErrors != t.wantVetErrors { - stdlog.Fatalf("resp.VetErrs = %q, want %q", resp.VetErrors, t.wantVetErrors) + stdlog.Printf("resp.VetErrs = %q, want %q", resp.VetErrors, t.wantVetErrors) + failed = true + continue } if t.withVet && (resp.VetErrors != "") == resp.VetOK { - stdlog.Fatalf("resp.VetErrs & VetOK inconsistent; VetErrs = %q; VetOK = %v", resp.VetErrors, resp.VetOK) + stdlog.Printf("resp.VetErrs & VetOK inconsistent; VetErrs = %q; VetOK = %v", resp.VetErrors, resp.VetOK) + failed = true + continue } if len(resp.Events) == 0 { - stdlog.Fatalf("unexpected output: %q, want %q", "", t.want) + stdlog.Printf("unexpected output: %q, want %q", "", t.want) + failed = true + continue } var b strings.Builder for _, e := range resp.Events { b.WriteString(e.Message) } - if !strings.Contains(b.String(), t.want) { - stdlog.Fatalf("unexpected output: %q, want %q", b.String(), t.want) + if t.wantFunc != nil { + if err := t.wantFunc(b.String()); err != nil { + stdlog.Printf("%v\n", err) + failed = true + } + } else { + if !strings.Contains(b.String(), t.want) { + stdlog.Printf("unexpected output: %q, want %q", b.String(), t.want) + failed = true + } } } + if failed { + stdlog.Fatalf("FAILED") + } fmt.Println("OK") } @@ -190,11 +229,16 @@ import ( func main() { filepath.Walk("/", func(path string, info os.FileInfo, err error) error { + if path == "/proc" || path == "/sys" { + return filepath.SkipDir + } fmt.Println(path) return nil }) } -`, want: `/ +`, wantFunc: func(got string) error { + // The environment for the old nacl sandbox: + if strings.TrimSpace(got) == `/ /dev /dev/null /dev/random @@ -211,8 +255,31 @@ func main() { /usr/local/go /usr/local/go/lib /usr/local/go/lib/time -/usr/local/go/lib/time/zoneinfo.zip`}, - +/usr/local/go/lib/time/zoneinfo.zip` { + return nil + } + have := map[string]bool{} + for _, f := range strings.Split(got, "\n") { + have[f] = true + } + for _, expect := range []string{ + "/.dockerenv", + "/__runsc_containers__", + "/etc/hostname", + "/dev/zero", + "/lib/ld-linux-x86-64.so.2", + "/lib/libc.so.6", + "/etc/nsswitch.conf", + "/bin/env", + "/tmpfs", + } { + if !have[expect] { + return fmt.Errorf("missing expected sandbox file %q; got:\n%s", expect, got) + } + } + return nil + }, + }, { name: "test_passes", prog: ` @@ -404,7 +471,9 @@ func main() { package main import ("fmt"; "github.com/bradfitz/iter") func main() { for i := range iter.N(5) { fmt.Println(i) } } -`, want: "0\n1\n2\n3\n4\n"}, +`, + want: "0\n1\n2\n3\n4\n", + }, { name: "compile_with_vet", diff --git a/vet.go b/vet.go index a9cae12a..032a5aa2 100644 --- a/vet.go +++ b/vet.go @@ -5,6 +5,7 @@ package main import ( + "context" "fmt" "io/ioutil" "os" @@ -21,7 +22,7 @@ import ( // the /compile (compileAndRun) handler instead with the WithVet // boolean set. This code path doesn't support modules and only exists // as a temporary compatiblity bridge to older javascript clients. -func vetCheck(req *request) (*response, error) { +func vetCheck(ctx context.Context, req *request) (*response, error) { tmpDir, err := ioutil.TempDir("", "vet") if err != nil { return nil, fmt.Errorf("error creating temp directory: %v", err) From a0e4bb3df9498d4508a95cfad6bfe455ad04552a Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 8 Jan 2020 22:10:42 +0000 Subject: [PATCH 037/148] sandbox: add missing word to package doc Change-Id: Iedc8686e22c291ca4e6aca3b730f6a94859bf974 Reviewed-on: https://go-review.googlesource.com/c/playground/+/213897 Reviewed-by: Ian Lance Taylor --- sandbox/sandbox.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index be7ab6d5..542e721c 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -3,9 +3,9 @@ // license that can be found in the LICENSE file. // The sandbox program is an HTTP server that receives untrusted -// linux/amd64 in a POST request and then executes them in a gvisor -// sandbox using Docker, returning the output as a response to the -// POST. +// linux/amd64 binaries in a POST request and then executes them in +// a gvisor sandbox using Docker, returning the output as a response +// to the POST. // // It's part of the Go playground (https://play.golang.org/). package main From 33fdd753e3a192794852ef632284c55a8fd83643 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 9 Jan 2020 20:57:07 +0000 Subject: [PATCH 038/148] playground: run in golang VPC network Also: * remove an outdated TODO. * pre-pull untrusted environment docker image on start-up to avoid stderr spam (and latency) later * update an ssh key for debugging (project-wide SSH keys are being ignored for some reason) * add a healthchecking TODO Updates golang/go#25224 Change-Id: I5a7e0c0f3d8f08c210783ecf4c1999586d13f576 Reviewed-on: https://go-review.googlesource.com/c/playground/+/214178 Reviewed-by: Alexander Rakoczy --- app.yaml | 5 ++++- sandbox/sandbox.go | 10 +++++++--- sandbox/sandbox.tf | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app.yaml b/app.yaml index c6791f8f..967b168f 100644 --- a/app.yaml +++ b/app.yaml @@ -2,6 +2,9 @@ service: play runtime: custom env: flex +network: + name: projects/golang-org/global/networks/golang + resources: cpu: 2 memory_gb: 2 @@ -14,4 +17,4 @@ readiness_check: check_interval_sec: 10 env_variables: - MEMCACHED_ADDR: 'memcached:11211' + MEMCACHED_ADDR: 'memcached-play-golang:11211' diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 542e721c..204fee4f 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -38,6 +38,7 @@ var ( mode = flag.String("mode", "server", "Whether to run in \"server\" mode or \"contained\" mode. The contained mode is used internally by the server mode.") dev = flag.Bool("dev", false, "run in dev mode (show help messages)") numWorkers = flag.Int("workers", runtime.NumCPU(), "number of parallel gvisor containers to pre-spin up & let run concurrently") + container = flag.String("untrusted-container", "gcr.io/golang-org/playground-sandbox-gvisor:latest", "container image name that hosts the untrusted binary under gvisor") ) const ( @@ -112,10 +113,11 @@ func main() { } if *dev { log.Printf("Running in dev mode; container published to host at: http://localhost:8080/") - // TODO: XXXX FIXME: this is no longer the protocol since the addition of the processMeta JSON header, - // so write a client program to do this instead? log.Printf("Run a binary with: curl -v --data-binary @/home/bradfitz/hello http://localhost:8080/run\n") } else { + if out, err := exec.Command("docker", "pull", *container).CombinedOutput(); err != nil { + log.Fatalf("error pulling %s: %v, %s", *container, err, out) + } log.Printf("Listening on %s", *listenAddr) } @@ -135,6 +137,8 @@ func handleSignals() { func healthHandler(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "OK\n") + // TODO: more? split into liveness & readiness checks? check + // number of active/stuck containers, memory? } func rootHandler(w http.ResponseWriter, r *http.Request) { @@ -290,7 +294,7 @@ func startContainer(ctx context.Context) (c *Container, err error) { "--network=none", "--memory="+fmt.Sprint(memoryLimitBytes), - "gcr.io/golang-org/playground-sandbox-gvisor:latest", + *container, "--mode=contained") stdin, err = cmd.StdinPipe() if err != nil { diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf index 61029c12..b13adef5 100644 --- a/sandbox/sandbox.tf +++ b/sandbox/sandbox.tf @@ -37,7 +37,7 @@ resource "google_compute_instance_template" "inst_tmpl" { name = "play-sandbox-tmpl" machine_type = "n1-standard-1" metadata = { - "ssh-keys" = "bradfitz:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCtL4Di+zypBIRmvohzfC4mOzETaz/DtzANPKir/mUE1QiC8HuL8BDpIu3rZZY1gAnQf3IZMpgQYgx90TZskgWfi7xLN2mDh2iBZB0KUDlpNpwn1SzSUTQU10XV5mOLm1B7L+w3QXA1wz7kMjmztxZNli/tvZI5BtAX6c58c4Rn0pjQTPeEVPMMvEj6zPUy4QInWTgtrbTj3On/e95F3ZmUjEKFfjeFxVPh7fF6mSygzcxEyGYxTbMdUod/dNbZD/7HY5eTNQPSvjp+GaoodbFEdYWnk9vtoX/2VWYo0J/+w6oHprDzj7dPzKeJeCjpnKra7DyCS/RxIIPt5Giwdj2F bradfitz@bradfitz-glinux-desktop.sea.corp.google.com" + "ssh-keys" = "bradfitz:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDaRpEbckQ+harGnrKUjk3JziwYqvz2bRNn0ngpzROaeCwm1XetDby/fgmQruZE/OBpbeOaCOd/yyP89Oer9CJx41AFEfHbudePZti/y+fmZ05N+QoBSAG0JtYWVydIjAjCenKBbNrYmwcQ840uNdIv9Ztqu3lbO/syMgcajappzdqMlwVZuHTJUe1JQD355PiinFHPTa7l0MrZPfiSsBdiTGmO39iVa312yshu6dZAvDgRL+bgIzTL6udPL/cVq+zlkvoZbzC4ajuZs4w2in+kqXHQSxbKHlXOhPrej1fwhspm+0Y7hEZOaN5Juc5GseNCHImtJh1rei1Qa4U/nTjt bradfitz@bradfitz-dev" "gce-container-declaration" = data.local_file.konlet.content "user-data" = data.local_file.cloud_init.content } From 62f81bd9157bf7f29142fc0030f1a7a62bd68edd Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 9 Jan 2020 23:05:00 +0000 Subject: [PATCH 039/148] sandbox: test docker connection & pull image before starting workers The docker pull added in CL 214178 was too late. The docker connection test can be whenever, but it's better to test it before we start failing for other reasons resulting from a lack of working Docker. Change-Id: Iff7120ac722c1c583ade98b8f27774d7d6293bf5 Reviewed-on: https://go-review.googlesource.com/c/playground/+/214237 Reviewed-by: Alexander Rakoczy --- sandbox/sandbox.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 204fee4f..9cbb73a9 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -105,7 +105,6 @@ func main() { readyContainer = make(chan *Container, *numWorkers) runSem = make(chan struct{}, *numWorkers) - go makeWorkers() go handleSignals() if out, err := exec.Command("docker", "version").CombinedOutput(); err != nil { @@ -125,6 +124,9 @@ func main() { http.HandleFunc("/healthz", healthHandler) http.HandleFunc("/", rootHandler) http.HandleFunc("/run", runHandler) + + go makeWorkers() + log.Fatal(http.ListenAndServe(*listenAddr, nil)) } From 0eefa27dcbc5bab25c7ce1c50baf6e504f783bf8 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 14 Jan 2020 20:01:12 +0000 Subject: [PATCH 040/148] sandbox: create larger VMs, add instance tags to permits health checks The GCP VMs were "failing" health checks before due to the firewall not allowing GCP's health checks. Added, per https://cloud.google.com/load-balancing/docs/health-checks#firewall_rules I just added both types of firewall rules (for legacy and non-legacy) so we can adjust which type of health checks we use in the future. Updates golang/go#25224 Change-Id: Ie8e585f76d97e770f1993b945e7a53732aec2cb8 Reviewed-on: https://go-review.googlesource.com/c/playground/+/214681 Reviewed-by: Alexander Rakoczy --- sandbox/sandbox.tf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf index b13adef5..e9554dd1 100644 --- a/sandbox/sandbox.tf +++ b/sandbox/sandbox.tf @@ -1,5 +1,8 @@ # TODO: move the network configuration into terraform too? It was created by hand with: # gcloud compute networks subnets update golang --region=us-central1 --enable-private-ip-google-access +# +# Likewise, the firewall rules for health checking were created imperatively based on +# https://cloud.google.com/load-balancing/docs/health-checks#firewall_rules terraform { backend "gcs" { @@ -35,7 +38,7 @@ data "google_compute_image" "cos" { resource "google_compute_instance_template" "inst_tmpl" { name = "play-sandbox-tmpl" - machine_type = "n1-standard-1" + machine_type = "n1-standard-8" metadata = { "ssh-keys" = "bradfitz:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDaRpEbckQ+harGnrKUjk3JziwYqvz2bRNn0ngpzROaeCwm1XetDby/fgmQruZE/OBpbeOaCOd/yyP89Oer9CJx41AFEfHbudePZti/y+fmZ05N+QoBSAG0JtYWVydIjAjCenKBbNrYmwcQ840uNdIv9Ztqu3lbO/syMgcajappzdqMlwVZuHTJUe1JQD355PiinFHPTa7l0MrZPfiSsBdiTGmO39iVa312yshu6dZAvDgRL+bgIzTL6udPL/cVq+zlkvoZbzC4ajuZs4w2in+kqXHQSxbKHlXOhPrej1fwhspm+0Y7hEZOaN5Juc5GseNCHImtJh1rei1Qa4U/nTjt bradfitz@bradfitz-dev" "gce-container-declaration" = data.local_file.konlet.content @@ -44,6 +47,9 @@ resource "google_compute_instance_template" "inst_tmpl" { network_interface { network = "golang" } + # Allow both "non-legacy" and "legacy" health checks, so we can change types in the future. + # See https://cloud.google.com/load-balancing/docs/health-checks + tags = ["allow-health-checks", "allow-network-lb-health-checks"] service_account { scopes = ["storage-ro"] } From 2f45547a763e80005aba046b3bbfb61f599af98c Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Tue, 14 Jan 2020 16:00:23 -0500 Subject: [PATCH 041/148] sandbox: add logging-write scope to service account This should remove another speedbump in viewing logs from sandbox containers. Updates golang/go#25224 Change-Id: I24be482ce90b189b0b74dab00a26c6b816592b9f Reviewed-on: https://go-review.googlesource.com/c/playground/+/214797 Reviewed-by: Brad Fitzpatrick --- sandbox/sandbox.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf index e9554dd1..e6c84b35 100644 --- a/sandbox/sandbox.tf +++ b/sandbox/sandbox.tf @@ -51,7 +51,7 @@ resource "google_compute_instance_template" "inst_tmpl" { # See https://cloud.google.com/load-balancing/docs/health-checks tags = ["allow-health-checks", "allow-network-lb-health-checks"] service_account { - scopes = ["storage-ro"] + scopes = ["logging-write", "storage-ro"] } disk { source_image = data.google_compute_image.cos.self_link From bca227fa3d29c3672596cc615c7874db85cd01d1 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 14 Jan 2020 21:10:03 +0000 Subject: [PATCH 042/148] sandbox: add header to stderr output, to skip anything that comes before it Updates golang/go#25224 Change-Id: Ib4e3bf2f2cf002b044a2d3a98de3c9dfa4b179d2 Reviewed-on: https://go-review.googlesource.com/c/playground/+/214682 Reviewed-by: Alexander Rakoczy --- sandbox/sandbox.go | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 9cbb73a9..d43f3e96 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -51,9 +51,14 @@ const ( var errTooMuchOutput = errors.New("Output too large") // containedStartMessage is the first thing written to stdout by the -// contained process when it starts up. This lets the parent HTTP +// gvisor-contained process when it starts up. This lets the parent HTTP // server know that a particular container is ready to run a binary. -const containedStartMessage = "started\n" +const containedStartMessage = "golang-gvisor-process-started\n" + +// containedStderrHeader is written to stderr after the gvisor-contained process +// successfully reads the processMeta JSON line + executable binary from stdin, +// but before it's run. +var containedStderrHeader = []byte("golang-gvisor-process-got-input\n") var ( readyContainer chan *Container @@ -185,6 +190,10 @@ func runInGvisor() { log.Fatalf("error decoding JSON meta: %v", err) } + if _, err := os.Stderr.Write(containedStderrHeader); err != nil { + log.Fatalf("writing header to stderr: %v", err) + } + // As part of a temporary transition plan, we also support // running nacl binaries in this sandbox. The point isn't to // double sandbox things as much as it is to let us transition @@ -211,7 +220,6 @@ func runInGvisor() { os.Remove(binPath) // not that it matters much, this container will be nuked os.Exit(errExitCode(err)) return - } func makeWorkers() { @@ -503,25 +511,9 @@ func sendResponse(w http.ResponseWriter, r *sandboxtypes.Response) { // cleanStderr removes spam stderr lines from the beginning of x // and returns a slice of x. func cleanStderr(x []byte) []byte { - for { - nl := bytes.IndexByte(x, '\n') - if nl == -1 || !isSpamStderrLine(x[:nl+1]) { - return x - } - x = x[nl+1:] + i := bytes.Index(x, containedStderrHeader) + if i == -1 { + return x } -} - -var warningPrefix = []byte("WARNING: ") - -// isSpamStderrLine reports whether line is a spammy line of stderr -// output from Docker. Currently it only matches things starting with -// "WARNING: " like: -// WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap. -// -// TODO: remove this and instead just make the child process start by -// writing a known header to stderr, then have parent skip everything -// before that unique header. -func isSpamStderrLine(line []byte) bool { - return bytes.HasPrefix(line, warningPrefix) + return x[i+len(containedStderrHeader):] } From 8445a0951ef1606dfaacb787ee7de23ce08a359c Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Thu, 16 Jan 2020 17:05:39 +0000 Subject: [PATCH 043/148] playground: slurp binary to memory in gvisor mode Slurp the binary to memory before POSTing it. This simplifies retries a bit (doesn't need to re-open the file) and sets a Content-Length header, and checks the file size early, rather than having the backend complain. None of this should matter for correctness (and unnecessarily uses more memory than before), but we're sometimes seeing timeout errors followed by this frontend returning (and caching!) an empty response where the backend never sees a binary arrive, so this is somewhat a shot in the dark for now. Something's up with our internal load balancer, perhaps. Still debugging. Also update/add some comments. Updates golang/go#25224 Change-Id: I12f08db8ac77743b555fe7ef56bccf8cbc45742c Reviewed-on: https://go-review.googlesource.com/c/playground/+/215058 Reviewed-by: Alexander Rakoczy --- sandbox.go | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/sandbox.go b/sandbox.go index e559725b..e55e8144 100644 --- a/sandbox.go +++ b/sandbox.go @@ -370,8 +370,11 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { } } - // TODO: remove all this once Go 1.14 is out. This is a transitional/debug step - // to support both nacl & gvisor temporarily. + // TODO: simplify this once Go 1.14 is out. We should remove + // the //play:gvisor substring hack and DEBUG_FORCE_GVISOR and + // instead implement https://golang.org/issue/33629 to + // officially support different Go versions (Go tip + past two + // releases). useGvisor := os.Getenv("GO_VERSION") >= "go1.14" || os.Getenv("DEBUG_FORCE_GVISOR") == "1" || strings.Contains(req.Body, "//play:gvisor\n") @@ -437,12 +440,18 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { rec := new(Recorder) var exitCode int if useGvisor { - f, err := os.Open(exe) + const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify? + if fi, err := os.Stat(exe); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { + if err != nil { + return nil, fmt.Errorf("failed to stat binary: %v", err) + } + return nil, fmt.Errorf("invalid binary size %d", fi.Size()) + } + exeBytes, err := ioutil.ReadFile(exe) if err != nil { return nil, err } - defer f.Close() - req, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), f) + req, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) if err != nil { return nil, err } @@ -450,7 +459,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { if testParam != "" { req.Header.Add("X-Argument", testParam) } - req.GetBody = func() (io.ReadCloser, error) { return os.Open(exe) } + req.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } res, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -503,6 +512,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { } var vetOut string if req.WithVet { + // TODO: do this concurrently with the execution to reduce latency. vetOut, err = vetCheckInDir(tmpDir, goPath, useModules) if err != nil { return nil, fmt.Errorf("running vet: %v", err) @@ -559,6 +569,11 @@ func (s *server) healthCheck() error { return nil } +// sandboxBackendURL returns the URL of the sandbox backend that +// executes binaries. This backend is required for Go 1.14+ (where it +// executes using gvisor, since Native Client support is removed). +// +// This function either returns a non-empty string or it panics. func sandboxBackendURL() string { if v := os.Getenv("SANDBOX_BACKEND_URL"); v != "" { return v From 1e9c260e0fcde94d918f29e9c0ac1b2650de4960 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 24 Jan 2020 20:32:00 +0000 Subject: [PATCH 044/148] sandbox, internal/gcpdial: dial sandbox backends directly, add health check Don't use GCP internal load balancers or health checkers at the moment because of reasons. Updates golang/go#25224 Change-Id: If443c25a8a40582f00a1bb51923214ab3b891b28 Reviewed-on: https://go-review.googlesource.com/c/playground/+/216397 Reviewed-by: Alexander Rakoczy --- go.mod | 1 + internal/gcpdial/gcpdial.go | 303 ++++++++++++++++++++ internal/gcpdial/gcpdialtool/gcpdialtool.go | 42 +++ sandbox.go | 46 ++- sandbox/sandbox.go | 67 ++++- sandbox/sandbox.tf | 47 +-- 6 files changed, 456 insertions(+), 50 deletions(-) create mode 100644 internal/gcpdial/gcpdial.go create mode 100644 internal/gcpdial/gcpdialtool/gcpdialtool.go diff --git a/go.mod b/go.mod index 039b6eaf..6fbe3816 100644 --- a/go.mod +++ b/go.mod @@ -8,5 +8,6 @@ require ( golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 golang.org/x/mod v0.1.1-0.20191119225628-919e395dadcd golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e + google.golang.org/api v0.4.0 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 ) diff --git a/internal/gcpdial/gcpdial.go b/internal/gcpdial/gcpdial.go new file mode 100644 index 00000000..212e1496 --- /dev/null +++ b/internal/gcpdial/gcpdial.go @@ -0,0 +1,303 @@ +// Copyright 2020 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. + +// Package gcpdial monitors VM instance groups to let frontends dial +// them directly without going through an internal load balancer. +package gcpdial + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "log" + "math/rand" + "net/http" + "strings" + "sync" + "time" + + compute "google.golang.org/api/compute/v1" +) + +type Dialer struct { + lister instanceLister + + mu sync.Mutex + lastInstances []string // URLs of instances + prober map[string]*prober // URL of instance to its prober + ready map[string]string // URL of instance to ready IP +} + +type prober struct { + d *Dialer + instURL string + cancel func() // called by Dialer to shut down this dialer + ctx context.Context // context that's canceled from above + + pi *parsedInstance + + // owned by the probeLoop goroutine: + ip string + healthy bool +} + +func newProber(d *Dialer, instURL string) *prober { + ctx, cancel := context.WithCancel(context.Background()) + return &prober{ + d: d, + instURL: instURL, + cancel: cancel, + ctx: ctx, + } +} + +func (p *prober) probeLoop() { + log.Printf("start prober for %s", p.instURL) + defer log.Printf("end prober for %s", p.instURL) + + pi, err := parseInstance(p.instURL) + if err != nil { + log.Printf("gcpdial: prober %s: failed to parse: %v", p.instURL, err) + return + } + p.pi = pi + + t := time.NewTicker(15 * time.Second) + defer t.Stop() + for { + p.probe() + select { + case <-p.ctx.Done(): + return + case <-t.C: + } + } +} + +func (p *prober) probe() { + if p.ip == "" && !p.getIP() { + return + } + ctx, cancel := context.WithTimeout(p.ctx, 30*time.Second) + defer cancel() + req, err := http.NewRequest("GET", "http://"+p.ip+"/healthz", nil) + if err != nil { + log.Printf("gcpdial: prober %s: NewRequest: %v", p.instURL, err) + return + } + req = req.WithContext(ctx) + res, err := http.DefaultClient.Do(req) + if res != nil { + defer res.Body.Close() + defer io.Copy(ioutil.Discard, res.Body) + } + healthy := err == nil && res.StatusCode == http.StatusOK + if healthy == p.healthy { + // No change. + return + } + p.healthy = healthy + + p.d.mu.Lock() + defer p.d.mu.Unlock() + if healthy { + if p.d.ready == nil { + p.d.ready = map[string]string{} + } + p.d.ready[p.instURL] = p.ip + // TODO: possible optimization: trigger + // Dialer.PickIP waiters to wake up rather + // than them polling once a second. + } else { + delete(p.d.ready, p.instURL) + var why string + if err != nil { + why = err.Error() + } else { + why = res.Status + } + log.Printf("gcpdial: prober %s: no longer healthy; %v", p.instURL, why) + } +} + +// getIP populates p.ip and reports whether it did so. +func (p *prober) getIP() bool { + if p.ip != "" { + return true + } + ctx, cancel := context.WithTimeout(p.ctx, 30*time.Second) + defer cancel() + svc, err := compute.NewService(ctx) + if err != nil { + log.Printf("gcpdial: prober %s: NewService: %v", p.instURL, err) + return false + } + inst, err := svc.Instances.Get(p.pi.Project, p.pi.Zone, p.pi.Name).Context(ctx).Do() + if err != nil { + log.Printf("gcpdial: prober %s: Get: %v", p.instURL, err) + return false + } + var ip string + var other []string + for _, ni := range inst.NetworkInterfaces { + if strings.HasPrefix(ni.NetworkIP, "10.") { + ip = ni.NetworkIP + } else { + other = append(other, ni.NetworkIP) + } + } + if ip == "" { + log.Printf("gcpdial: prober %s: didn't find 10.x.x.x IP; found %q", p.instURL, other) + return false + } + p.ip = ip + return true +} + +// PickIP returns a randomly healthy IP, waiting until one is available, or until ctx expires. +func (d *Dialer) PickIP(ctx context.Context) (ip string, err error) { + for { + if ip, ok := d.pickIP(); ok { + return ip, nil + } + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(time.Second): + } + } +} + +func (d *Dialer) pickIP() (string, bool) { + d.mu.Lock() + defer d.mu.Unlock() + if len(d.ready) == 0 { + return "", false + } + num := rand.Intn(len(d.ready)) + for _, v := range d.ready { + if num > 0 { + num-- + continue + } + return v, true + } + panic("not reachable") +} + +func (d *Dialer) poll() { + for { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + res, err := d.lister.ListInstances(ctx) + cancel() + if err != nil { + log.Printf("gcpdial: polling %v: %v", d.lister, err) + time.Sleep(10 * time.Second) + continue + } + + want := map[string]bool{} // the res []string turned into a set + for _, instURL := range res { + want[instURL] = true + } + + d.mu.Lock() + // Stop + remove any health check probers that no longer appear in the + // instance group. + for instURL, prober := range d.prober { + if !want[instURL] { + prober.cancel() + delete(d.prober, instURL) + } + } + // And start any new health check probers that are newly added + // (or newly known at least) to the instance group. + for _, instURL := range res { + if _, ok := d.prober[instURL]; ok { + continue + } + p := newProber(d, instURL) + go p.probeLoop() + if d.prober == nil { + d.prober = map[string]*prober{} + } + d.prober[instURL] = p + } + d.lastInstances = res + d.mu.Unlock() + } +} + +// NewRegionInstanceGroupDialer returns a new dialer that dials named +// regional instance group in the provided project and region. +// +// It begins polling immediately, and there's no way to stop it. +// (Until we need one) +func NewRegionInstanceGroupDialer(project, region, group string) *Dialer { + d := &Dialer{ + lister: regionInstanceGroupLister{project, region, group}, + } + go d.poll() + return d +} + +// instanceLister is something that can list the current set of VMs. +// +// The idea is that we'll have both zonal and regional instance group listers, +// but currently we only have regionInstanceGroupLister below. +type instanceLister interface { + // ListInstances returns a list of instances in their API URL form. + // + // The API URL form is parseable by the parseInstance func. See its docs. + ListInstances(context.Context) ([]string, error) +} + +// regionInstanceGroupLister is an instanceLister implementation that watches a regional +// instance group for changes to its set of VMs. +type regionInstanceGroupLister struct { + project, region, group string +} + +func (rig regionInstanceGroupLister) ListInstances(ctx context.Context) (ret []string, err error) { + svc, err := compute.NewService(ctx) + if err != nil { + return nil, err + } + rigSvc := svc.RegionInstanceGroups + insts, err := rigSvc.ListInstances(rig.project, rig.region, rig.group, &compute.RegionInstanceGroupsListInstancesRequest{ + InstanceState: "RUNNING", + PortName: "", // all + }).Context(ctx).MaxResults(500).Do() + if err != nil { + return nil, err + } + // TODO: pagination for really large sets? Currently we truncate the results + // to the first 500 VMs, which seems like plenty for now. + // 500 is the maximum the API supports; see: + // https://pkg.go.dev/google.golang.org/api/compute/v1?tab=doc#RegionInstanceGroupsListInstancesCall.MaxResults + for _, it := range insts.Items { + ret = append(ret, it.Instance) + } + return ret, nil +} + +// parsedInstance contains the project, zone, and name of a VM. +type parsedInstance struct { + Project, Zone, Name string +} + +// parseInstance parses e.g. "https://www.googleapis.com/compute/v1/projects/golang-org/zones/us-central1-c/instances/playsandbox-7sj8" into its parts. +func parseInstance(u string) (*parsedInstance, error) { + const pfx = "https://www.googleapis.com/compute/v1/projects/" + if !strings.HasPrefix(u, pfx) { + return nil, fmt.Errorf("failed to parse instance %q; doesn't begin with %q", u, pfx) + } + u = u[len(pfx):] // "golang-org/zones/us-central1-c/instances/playsandbox-7sj8" + f := strings.Split(u, "/") + if len(f) != 5 || f[1] != "zones" || f[3] != "instances" { + return nil, fmt.Errorf("failed to parse instance %q; unexpected format", u) + } + return &parsedInstance{f[0], f[2], f[4]}, nil +} diff --git a/internal/gcpdial/gcpdialtool/gcpdialtool.go b/internal/gcpdial/gcpdialtool/gcpdialtool.go new file mode 100644 index 00000000..1bcbd6c4 --- /dev/null +++ b/internal/gcpdial/gcpdialtool/gcpdialtool.go @@ -0,0 +1,42 @@ +// Copyright 2020 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 gcpdialtool command is an interactive validation tool for the +// gcpdial packge. +package main + +import ( + "context" + "flag" + "log" + "os" + "time" + + "golang.org/x/playground/internal/gcpdial" +) + +var ( + proj = flag.String("project", "golang-org", "GCP project name") + region = flag.String("region", "us-central1", "GCP region") + group = flag.String("group", "play-sandbox-rigm", "regional instance group name") +) + +func main() { + flag.Parse() + log.SetOutput(os.Stdout) + log.SetFlags(log.Flags() | log.Lmicroseconds) + + log.Printf("starting") + d := gcpdial.NewRegionInstanceGroupDialer(*proj, *region, *group) + + ctx := context.Background() + for { + ip, err := d.PickIP(ctx) + if err != nil { + log.Fatal(err) + } + log.Printf("picked %v", ip) + time.Sleep(time.Second) + } +} diff --git a/sandbox.go b/sandbox.go index e55e8144..58431d4b 100644 --- a/sandbox.go +++ b/sandbox.go @@ -20,6 +20,7 @@ import ( "go/token" "io" "io/ioutil" + "net" "net/http" "os" "os/exec" @@ -27,11 +28,13 @@ import ( "runtime" "strconv" "strings" + "sync" "text/template" "time" "cloud.google.com/go/compute/metadata" "github.com/bradfitz/gomemcache/memcache" + "golang.org/x/playground/internal/gcpdial" "golang.org/x/playground/sandbox/sandboxtypes" ) @@ -460,7 +463,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { req.Header.Add("X-Argument", testParam) } req.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } - res, err := http.DefaultClient.Do(req) + res, err := sandboxBackendClient().Do(req) if err != nil { return nil, err } @@ -586,6 +589,47 @@ func sandboxBackendURL() string { panic(fmt.Sprintf("no SANDBOX_BACKEND_URL environment and no default defined for project %q", id)) } +var sandboxBackendOnce struct { + sync.Once + c *http.Client +} + +func sandboxBackendClient() *http.Client { + sandboxBackendOnce.Do(initSandboxBackendClient) + return sandboxBackendOnce.c +} + +// initSandboxBackendClient runs from a sync.Once and initializes +// sandboxBackendOnce.c with the *http.Client we'll use to contact the +// sandbox execution backend. +func initSandboxBackendClient() { + id, _ := metadata.ProjectID() + switch id { + case "golang-org": + // For production, use a funky Transport dialer that + // contacts backend directly, without going through an + // internal load balancer, due to internal GCP + // reasons, which we might resolve later. This might + // be a temporary hack. + tr := http.DefaultTransport.(*http.Transport).Clone() + rigd := gcpdial.NewRegionInstanceGroupDialer("golang-org", "us-central1", "play-sandbox-rigm") + tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) { + if addr == "sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal:80" { + ip, err := rigd.PickIP(ctx) + if err != nil { + return nil, err + } + addr = net.JoinHostPort(ip, "80") // and fallthrough + } + var d net.Dialer + return d.DialContext(ctx, netw, addr) + } + sandboxBackendOnce.c = &http.Client{Transport: tr} + default: + sandboxBackendOnce.c = http.DefaultClient + } +} + const healthProg = ` package main diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index d43f3e96..41f1f9b8 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -96,6 +96,8 @@ func (c *Container) wait() { c.waitVal = c.cmd.Wait() } +var httpServer *http.Server + func main() { flag.Parse() if *mode == "contained" { @@ -132,7 +134,8 @@ func main() { go makeWorkers() - log.Fatal(http.ListenAndServe(*listenAddr, nil)) + httpServer = &http.Server{Addr: *listenAddr} + log.Fatal(httpServer.ListenAndServe()) } func handleSignals() { @@ -142,10 +145,68 @@ func handleSignals() { log.Fatalf("closing on signal %d: %v", s, s) } +var healthStatus struct { + sync.Mutex + lastCheck time.Time + lastVal error +} + +func getHealthCached() error { + healthStatus.Lock() + defer healthStatus.Unlock() + const recentEnough = 5 * time.Second + if healthStatus.lastCheck.After(time.Now().Add(-recentEnough)) { + return healthStatus.lastVal + } + + err := checkHealth() + if healthStatus.lastVal == nil && err != nil { + // On transition from healthy to unhealthy, close all + // idle HTTP connections so clients with them open + // don't reuse them. TODO: remove this if/when we + // switch away from direct load balancing between + // frontends and this sandbox backend. + httpServer.SetKeepAlivesEnabled(false) // side effect of closing all idle ones + httpServer.SetKeepAlivesEnabled(true) // and restore it back to normal + } + healthStatus.lastVal = err + healthStatus.lastCheck = time.Now() + return err +} + +// checkHealth does a health check, without any caching. It's called via +// getHealthCached. +func checkHealth() error { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + c, err := getContainer(ctx) + if err != nil { + return fmt.Errorf("failed to get a sandbox container: %v", err) + } + // TODO: execute something too? for now we just check that sandboxed containers + // are available. + closed := make(chan struct{}) + go func() { + c.Close() + close(closed) + }() + select { + case <-closed: + // success. + return nil + case <-ctx.Done(): + return fmt.Errorf("timeout closing sandbox container") + } +} + func healthHandler(w http.ResponseWriter, r *http.Request) { + // TODO: split into liveness & readiness checks? + if err := getHealthCached(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "health check failure: %v\n", err) + return + } io.WriteString(w, "OK\n") - // TODO: more? split into liveness & readiness checks? check - // number of active/stuck containers, memory? } func rootHandler(w http.ResponseWriter, r *http.Request) { diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf index e6c84b35..14de7cbc 100644 --- a/sandbox/sandbox.tf +++ b/sandbox/sandbox.tf @@ -1,8 +1,6 @@ # TODO: move the network configuration into terraform too? It was created by hand with: # gcloud compute networks subnets update golang --region=us-central1 --enable-private-ip-google-access # -# Likewise, the firewall rules for health checking were created imperatively based on -# https://cloud.google.com/load-balancing/docs/health-checks#firewall_rules terraform { backend "gcs" { @@ -47,9 +45,6 @@ resource "google_compute_instance_template" "inst_tmpl" { network_interface { network = "golang" } - # Allow both "non-legacy" and "legacy" health checks, so we can change types in the future. - # See https://cloud.google.com/load-balancing/docs/health-checks - tags = ["allow-health-checks", "allow-network-lb-health-checks"] service_account { scopes = ["logging-write", "storage-ro"] } @@ -76,7 +71,7 @@ resource "google_compute_region_autoscaler" "default" { autoscaling_policy { max_replicas = 10 - min_replicas = 2 + min_replicas = 3 cooldown_period = 60 cpu_utilization { @@ -101,49 +96,9 @@ resource "google_compute_region_instance_group_manager" "rigm" { name = "http" port = 80 } - - auto_healing_policies { - health_check = "${google_compute_health_check.default.self_link}" - initial_delay_sec = 30 - } } data "google_compute_region_instance_group" "rig" { provider = "google-beta" self_link = "${google_compute_region_instance_group_manager.rigm.instance_group}" } - -resource "google_compute_health_check" "default" { - name = "play-sandbox-rigm-health-check" - check_interval_sec = 5 - timeout_sec = 5 - healthy_threshold = 2 - unhealthy_threshold = 10 # 50 seconds - http_health_check { - request_path = "/healthz" - port = 80 - } -} - -resource "google_compute_region_backend_service" "default" { - name = "play-sandbox-backend-service" - region = "us-central1" - health_checks = ["${google_compute_health_check.default.self_link}"] - backend { - group = "${data.google_compute_region_instance_group.rig.self_link}" - } -} - -resource "google_compute_forwarding_rule" "default" { - name = "play-sandbox-fwd" - region = "us-central1" - network = "golang" - ports = ["80"] - load_balancing_scheme = "INTERNAL" - ip_protocol = "TCP" - backend_service = "${google_compute_region_backend_service.default.self_link}" - - # Adding a service label gives us a DNS name: - # sandbox.play-sandbox-fwd.il4.us-central1.lb.golang-org.internal - service_label = "sandbox" -} From 3f031d79ab7a1bc1981fcc1579bdde8757b6621b Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 28 Jan 2020 21:34:08 +0000 Subject: [PATCH 045/148] playground: warm up backend dialer at start Reduces latency for new frontend instances. Updates golang/go#25224 Change-Id: I5847f6d3dbfd09a2f2cc6e71548f55e6bcd31e52 Reviewed-on: https://go-review.googlesource.com/c/playground/+/216737 Reviewed-by: Alexander Rakoczy --- main.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.go b/main.go index 3bba9aba..6b1105e8 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,11 @@ func main() { if port == "" { port = "8080" } + + // Get the backend dialer warmed up. This starts + // RegionInstanceGroupDialer queries and health checks. + go sandboxBackendClient() + log.Printf("Listening on :%v ...", port) log.Fatalf("Error listening on :%v: %v", port, http.ListenAndServe(":"+port, s)) } From 09dfaa13f5de54c7281cfefb0aa4559e246d08b8 Mon Sep 17 00:00:00 2001 From: smasher164 Date: Sun, 26 Jan 2020 23:09:49 -0500 Subject: [PATCH 046/148] playground: add examples to demonstrate tricks As a way to document the playground's tricks, this change adds a dropdown of examples to the right of the "Share" button. They are as follows: - Hello, playground (default) - Tests - Multiple files - Display image - Sleep - Clear Fixes golang/go#32294. Change-Id: I6cdd54714d5638a932b2d392db1ea888468f5cec Reviewed-on: https://go-review.googlesource.com/c/playground/+/216359 Run-TryBot: Andrew Bonventre TryBot-Result: Gobot Gobot Reviewed-by: Andrew Bonventre --- Dockerfile | 1 + edit.html | 11 ++++++++++- examples/clear.txt | 18 ++++++++++++++++++ examples/hello.txt | 9 +++++++++ examples/image.txt | 40 ++++++++++++++++++++++++++++++++++++++++ examples/multi.txt | 21 +++++++++++++++++++++ examples/sleep.txt | 17 +++++++++++++++++ examples/test.txt | 37 +++++++++++++++++++++++++++++++++++++ server.go | 3 +++ static/style.css | 6 ++++++ 10 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 examples/clear.txt create mode 100644 examples/hello.txt create mode 100644 examples/image.txt create mode 100644 examples/multi.txt create mode 100644 examples/sleep.txt create mode 100644 examples/test.txt diff --git a/Dockerfile b/Dockerfile index 063f9a52..dafa0d4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -121,6 +121,7 @@ RUN mkdir /app COPY --from=build /go/bin/playground /app COPY edit.html /app COPY static /app/static +COPY examples /app/examples WORKDIR /app # Whether we allow third-party imports via proxy.golang.org: diff --git a/edit.html b/edit.html index 2b5ded86..719edabd 100644 --- a/edit.html +++ b/edit.html @@ -31,7 +31,8 @@ {{end}} 'enableHistory': true, 'enableShortcuts': true, - 'enableVet': true + 'enableVet': true, + 'toysEl': '.js-playgroundToysEl' }); playgroundEmbed({ 'codeEl': '#code', @@ -120,6 +121,14 @@ embed {{end}} +

diff --git a/examples/clear.txt b/examples/clear.txt new file mode 100644 index 00000000..c5381d71 --- /dev/null +++ b/examples/clear.txt @@ -0,0 +1,18 @@ +package main + +import ( + "fmt" + "strings" + "time" +) + +func main() { + const col = 30 + // Clear the screen by printing \x0c. + bar := fmt.Sprintf("\x0c[%%-%vs]", col) + for i := 0; i < col; i++ { + fmt.Printf(bar, strings.Repeat("=", i)+">") + time.Sleep(100 * time.Millisecond) + } + fmt.Printf(bar+" Done!", strings.Repeat("=", col)) +} diff --git a/examples/hello.txt b/examples/hello.txt new file mode 100644 index 00000000..8fd43ed1 --- /dev/null +++ b/examples/hello.txt @@ -0,0 +1,9 @@ +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, playground") +} diff --git a/examples/image.txt b/examples/image.txt new file mode 100644 index 00000000..4180a450 --- /dev/null +++ b/examples/image.txt @@ -0,0 +1,40 @@ +package main + +import ( + "bytes" + "encoding/base64" + "fmt" + "image" + "image/png" +) + +var favicon = []byte{ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, + 0x10, 0x00, 0x00, 0x00, 0x0f, 0x04, 0x03, 0x00, 0x00, 0x00, 0x1f, 0x5d, 0x52, 0x1c, 0x00, 0x00, 0x00, 0x0f, 0x50, + 0x4c, 0x54, 0x45, 0x7a, 0xdf, 0xfd, 0xfd, 0xff, 0xfc, 0x39, 0x4d, 0x52, 0x19, 0x16, 0x15, 0xc3, 0x8d, 0x76, 0xc7, + 0x36, 0x2c, 0xf5, 0x00, 0x00, 0x00, 0x40, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x95, 0xc9, 0xd1, 0x0d, 0xc0, 0x20, + 0x0c, 0x03, 0xd1, 0x23, 0x5d, 0xa0, 0x49, 0x17, 0x20, 0x4c, 0xc0, 0x10, 0xec, 0x3f, 0x53, 0x8d, 0xc2, 0x02, 0x9c, + 0xfc, 0xf1, 0x24, 0xe3, 0x31, 0x54, 0x3a, 0xd1, 0x51, 0x96, 0x74, 0x1c, 0xcd, 0x18, 0xed, 0x9b, 0x9a, 0x11, 0x85, + 0x24, 0xea, 0xda, 0xe0, 0x99, 0x14, 0xd6, 0x3a, 0x68, 0x6f, 0x41, 0xdd, 0xe2, 0x07, 0xdb, 0xb5, 0x05, 0xca, 0xdb, + 0xb2, 0x9a, 0xdd, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, +} + +// displayImage renders an image to the playground's console by +// base64-encoding the encoded image and printing it to stdout +// with the prefix "IMAGE:". +func displayImage(m image.Image) { + var buf bytes.Buffer + err := png.Encode(&buf, m) + if err != nil { + panic(err) + } + fmt.Println("IMAGE:" + base64.StdEncoding.EncodeToString(buf.Bytes())) +} + +func main() { + m, err := png.Decode(bytes.NewReader(favicon)) + if err != nil { + panic(err) + } + displayImage(m) +} diff --git a/examples/multi.txt b/examples/multi.txt new file mode 100644 index 00000000..ad41446f --- /dev/null +++ b/examples/multi.txt @@ -0,0 +1,21 @@ +package main + +import ( + "play.ground/foo" +) + +func main() { + foo.Bar() +} + +-- go.mod -- +module play.ground + +-- foo/foo.go -- +package foo + +import "fmt" + +func Bar() { + fmt.Println("This function lives in an another file!") +} diff --git a/examples/sleep.txt b/examples/sleep.txt new file mode 100644 index 00000000..d68a3d8e --- /dev/null +++ b/examples/sleep.txt @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + "math/rand" + "time" +) + +func main() { + for i := 0; i < 10; i++ { + dur := time.Duration(rand.Intn(1000)) * time.Millisecond + fmt.Printf("Sleeping for %v\n", dur) + // Sleep for a random duration between 0-1000ms + time.Sleep(dur) + } + fmt.Println("Done!") +} diff --git a/examples/test.txt b/examples/test.txt new file mode 100644 index 00000000..4bf86e90 --- /dev/null +++ b/examples/test.txt @@ -0,0 +1,37 @@ +package main + +import ( + "testing" +) + +// LastIndex returns the index of the last instance of x in list, or +// -1 if x is not present. The loop condition has a fault that +// causes somes tests to fail. Change it to i >= 0 to see them pass. +func LastIndex(list []int, x int) int { + for i := len(list) - 1; i > 0; i-- { + if list[i] == x { + return i + } + } + return -1 +} + +func TestLastIndex(t *testing.T) { + tests := []struct { + list []int + x int + want int + }{ + {list: []int{1}, x: 1, want: 0}, + {list: []int{1, 1}, x: 1, want: 1}, + {list: []int{2, 1}, x: 2, want: 0}, + {list: []int{1, 2, 1, 1}, x: 2, want: 1}, + {list: []int{1, 1, 1, 2, 2, 1}, x: 3, want: -1}, + {list: []int{3, 1, 2, 2, 1, 1}, x: 3, want: 0}, + } + for _, tt := range tests { + if got := LastIndex(tt.list, tt.x); got != tt.want { + t.Errorf("LastIndex(%v, %v) = %v, want %v", tt.list, tt.x, got, tt.want) + } + } +} diff --git a/server.go b/server.go index d3a491b1..f091933d 100644 --- a/server.go +++ b/server.go @@ -59,6 +59,9 @@ func (s *server) init() { staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))) s.mux.Handle("/static/", staticHandler) + + examplesHandler := http.StripPrefix("/doc/play/", http.FileServer(http.Dir("./examples"))) + s.mux.Handle("/doc/play/", examplesHandler) } func (s *server) handlePlaygroundJS(w http.ResponseWriter, r *http.Request) { diff --git a/static/style.css b/static/style.css index 1cd00e81..ac93cf0d 100644 --- a/static/style.css +++ b/static/style.css @@ -174,3 +174,9 @@ input[type=button], top: 10px; right: 10px; } + +#controls select { + font-size: 0.875rem; + border: 0.0625rem solid #375EAB; + margin: 0; +} \ No newline at end of file From fa32453c2afd17eb329251bbba28d1fab29a63a0 Mon Sep 17 00:00:00 2001 From: mo4islona Date: Sun, 8 Sep 2019 18:24:41 +0300 Subject: [PATCH 047/148] playground: enable CORS on /fmt and /p/ routes Previously, only the /vet, /compile, and /share routes supported Cross-Origin Resource Sharing (CORS) from any domain. This change enables CORS on the /p/ and /fmt endpoints as well. That makes it possible for third-party sites to load and format playground snippets from the frontend. Fixes golang/go#35019 Change-Id: I185f518257082cbb5ccd848b410ff408bc077340 Reviewed-on: https://go-review.googlesource.com/c/playground/+/193798 Reviewed-by: Alexander Rakoczy --- edit.go | 6 +++++ fmt.go | 5 ++++ fmt_test.go | 62 ++++++++++++++++++++++++++++++++++---------------- server_test.go | 24 +++++++++++++------ 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/edit.go b/edit.go index 81923e4a..c8cb9740 100644 --- a/edit.go +++ b/edit.go @@ -26,6 +26,12 @@ type editData struct { } func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == "OPTIONS" { + // This is likely a pre-flight CORS request. + return + } + // Redirect foo.play.golang.org to play.golang.org. if strings.HasSuffix(r.Host, "."+hostname) { http.Redirect(w, r, "https://"+hostname, http.StatusFound) diff --git a/fmt.go b/fmt.go index 88c07466..91027be7 100644 --- a/fmt.go +++ b/fmt.go @@ -21,6 +21,11 @@ type fmtResponse struct { } func handleFmt(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == "OPTIONS" { + // This is likely a pre-flight CORS request. + return + } w.Header().Set("Content-Type", "application/json") fs, err := splitFiles([]byte(r.FormValue("body"))) diff --git a/fmt_test.go b/fmt_test.go index e601dc3d..ceea152e 100644 --- a/fmt_test.go +++ b/fmt_test.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" + "net/http" "net/http/httptest" "net/url" "strings" @@ -15,49 +16,62 @@ import ( func TestHandleFmt(t *testing.T) { for _, tt := range []struct { name string + method string body string imports bool want string wantErr string }{ { - name: "classic", - body: " package main\n func main( ) { }\n", - want: "package main\n\nfunc main() {}\n", + name: "OPTIONS no-op", + method: http.MethodOptions, + }, + { + name: "classic", + method: http.MethodPost, + body: " package main\n func main( ) { }\n", + want: "package main\n\nfunc main() {}\n", }, { name: "classic_goimports", + method: http.MethodPost, body: " package main\nvar _ = fmt.Printf", imports: true, want: "package main\n\nimport \"fmt\"\n\nvar _ = fmt.Printf\n", }, { - name: "single_go_with_header", - body: "-- prog.go --\n package main", - want: "-- prog.go --\npackage main\n", + name: "single_go_with_header", + method: http.MethodPost, + body: "-- prog.go --\n package main", + want: "-- prog.go --\npackage main\n", }, { - name: "multi_go_with_header", - body: "-- prog.go --\n package main\n\n\n-- two.go --\n package main\n var X = 5", - want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n", + name: "multi_go_with_header", + method: http.MethodPost, + body: "-- prog.go --\n package main\n\n\n-- two.go --\n package main\n var X = 5", + want: "-- prog.go --\npackage main\n-- two.go --\npackage main\n\nvar X = 5\n", }, { - name: "multi_go_without_header", - body: " package main\n\n\n-- two.go --\n package main\n var X = 5", - want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n", + name: "multi_go_without_header", + method: http.MethodPost, + body: " package main\n\n\n-- two.go --\n package main\n var X = 5", + want: "package main\n-- two.go --\npackage main\n\nvar X = 5\n", }, { - name: "single_go.mod_with_header", - body: "-- go.mod --\n module \"foo\" ", - want: "-- go.mod --\nmodule foo\n", + name: "single_go.mod_with_header", + method: http.MethodPost, + body: "-- go.mod --\n module \"foo\" ", + want: "-- go.mod --\nmodule foo\n", }, { - name: "multi_go.mod_with_header", - body: "-- a/go.mod --\n module foo\n\n\n-- b/go.mod --\n module \"bar\"", - want: "-- a/go.mod --\nmodule foo\n-- b/go.mod --\nmodule bar\n", + name: "multi_go.mod_with_header", + method: http.MethodPost, + body: "-- a/go.mod --\n module foo\n\n\n-- b/go.mod --\n module \"bar\"", + want: "-- a/go.mod --\nmodule foo\n-- b/go.mod --\nmodule bar\n", }, { - name: "only_format_go_and_go.mod", + name: "only_format_go_and_go.mod", + method: http.MethodPost, body: " package main \n\n\n" + "-- go.mod --\n module foo \n\n\n" + "-- plain.txt --\n plain text \n\n\n", @@ -65,33 +79,39 @@ func TestHandleFmt(t *testing.T) { }, { name: "error_gofmt", + method: http.MethodPost, body: "package 123\n", wantErr: "prog.go:1:9: expected 'IDENT', found 123", }, { name: "error_gofmt_with_header", + method: http.MethodPost, body: "-- dir/one.go --\npackage 123\n", wantErr: "dir/one.go:1:9: expected 'IDENT', found 123", }, { name: "error_goimports", + method: http.MethodPost, body: "package 123\n", imports: true, wantErr: "prog.go:1:9: expected 'IDENT', found 123", }, { name: "error_goimports_with_header", + method: http.MethodPost, body: "-- dir/one.go --\npackage 123\n", imports: true, wantErr: "dir/one.go:1:9: expected 'IDENT', found 123", }, { name: "error_go.mod", + method: http.MethodPost, body: "-- go.mod --\n123\n", wantErr: "go.mod:1: unknown directive: 123", }, { name: "error_go.mod_with_header", + method: http.MethodPost, body: "-- dir/go.mod --\n123\n", wantErr: "dir/go.mod:1: unknown directive: 123", }, @@ -110,6 +130,10 @@ func TestHandleFmt(t *testing.T) { if resp.StatusCode != 200 { t.Fatalf("code = %v", resp.Status) } + corsHeader := "Access-Control-Allow-Origin" + if got, want := resp.Header.Get(corsHeader), "*"; got != want { + t.Errorf("Header %q: got %q; want %q", corsHeader, got, want) + } if ct := resp.Header.Get("Content-Type"); ct != "application/json" { t.Fatalf("Content-Type = %q; want application/json", ct) } diff --git a/server_test.go b/server_test.go index 40ba0654..a7d9fac2 100644 --- a/server_test.go +++ b/server_test.go @@ -51,24 +51,30 @@ func TestEdit(t *testing.T) { testCases := []struct { desc string + method string url string statusCode int headers map[string]string respBody []byte }{ - {"foo.play.golang.org to play.golang.org", "https://foo.play.golang.org", http.StatusFound, map[string]string{"Location": "https://play.golang.org"}, nil}, - {"Non-existent page", "https://play.golang.org/foo", http.StatusNotFound, nil, nil}, - {"Unknown snippet", "https://play.golang.org/p/foo", http.StatusNotFound, nil, nil}, - {"Existing snippet", "https://play.golang.org/p/" + id, http.StatusOK, nil, nil}, - {"Plaintext snippet", "https://play.golang.org/p/" + id + ".go", http.StatusOK, nil, barBody}, - {"Download snippet", "https://play.golang.org/p/" + id + ".go?download=true", http.StatusOK, map[string]string{"Content-Disposition": fmt.Sprintf(`attachment; filename="%s.go"`, id)}, barBody}, + {"OPTIONS no-op", http.MethodOptions, "https://play.golang.org/p/foo", http.StatusOK, nil, nil}, + {"foo.play.golang.org to play.golang.org", http.MethodGet, "https://foo.play.golang.org", http.StatusFound, map[string]string{"Location": "https://play.golang.org"}, nil}, + {"Non-existent page", http.MethodGet, "https://play.golang.org/foo", http.StatusNotFound, nil, nil}, + {"Unknown snippet", http.MethodGet, "https://play.golang.org/p/foo", http.StatusNotFound, nil, nil}, + {"Existing snippet", http.MethodGet, "https://play.golang.org/p/" + id, http.StatusOK, nil, nil}, + {"Plaintext snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go", http.StatusOK, nil, barBody}, + {"Download snippet", http.MethodGet, "https://play.golang.org/p/" + id + ".go?download=true", http.StatusOK, map[string]string{"Content-Disposition": fmt.Sprintf(`attachment; filename="%s.go"`, id)}, barBody}, } for _, tc := range testCases { - req := httptest.NewRequest(http.MethodGet, tc.url, nil) + req := httptest.NewRequest(tc.method, tc.url, nil) w := httptest.NewRecorder() s.handleEdit(w, req) resp := w.Result() + corsHeader := "Access-Control-Allow-Origin" + if got, want := resp.Header.Get(corsHeader), "*"; got != want { + t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) + } if got, want := resp.StatusCode, tc.statusCode; got != want { t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) } @@ -115,6 +121,10 @@ func TestShare(t *testing.T) { w := httptest.NewRecorder() s.handleShare(w, req) resp := w.Result() + corsHeader := "Access-Control-Allow-Origin" + if got, want := resp.Header.Get(corsHeader), "*"; got != want { + t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) + } if got, want := resp.StatusCode, tc.statusCode; got != want { t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) } From 56823308f5190e16ec07d406387663a8694aa1c0 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Fri, 3 Apr 2020 15:46:47 -0400 Subject: [PATCH 048/148] playground: fix the build for Go 1.14.x The NaCL patches do not apply to Go 1.14. This should help fix deployments. As a result, NaCL mode is pinned at 1.13.x. In the Dockerfile, GO_VERSION will only apply to the gVisor flow going forward. Further simplifications will be made in a future CL. Updates golang/go#25224 Change-Id: I9241416670646d775ef2c81183eaaf9f1cc42442 Reviewed-on: https://go-review.googlesource.com/c/playground/+/227347 Reviewed-by: Andrew Bonventre --- Dockerfile | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index dafa0d4e..ef329ea5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,8 @@ RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap -ENV GO_BOOTSTRAP_VERSION go1.13 -ARG GO_VERSION=go1.13 +ENV GO_BOOTSTRAP_VERSION go1.13.9 +ARG GO_VERSION=go1.14.1 ENV GO_VERSION ${GO_VERSION} # Fake time @@ -36,7 +36,7 @@ RUN mkdir -p $GOROOT_BOOTSTRAP RUN tar --strip=1 -C $GOROOT_BOOTSTRAP -vxzf /tmp/go.tar.gz # Fetch Go source for tag $GO_VERSION. -RUN git clone --depth=1 --branch=$GO_VERSION https://go.googlesource.com/go /usr/local/go +RUN git clone --depth=1 --branch=$GO_BOOTSTRAP_VERSION https://go.googlesource.com/go /usr/local/go # Apply the fake time and fake filesystem patches. RUN patch /usr/local/go/src/runtime/rt0_nacl_amd64p32.s /usr/local/playground/enable-fake-time.patch RUN cd /usr/local/go && $GOROOT_BOOTSTRAP/bin/go run misc/nacl/mkzip.go -p syscall /usr/local/playground/fake_fs.lst src/syscall/fstest_nacl.go @@ -74,10 +74,7 @@ FROM golang:1.13 AS temp_pre_go14 ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' RUN apt-get update && apt-get install -y --no-install-recommends ${BUILD_DEPS} -# go1.14beta1: -ENV GO_REV a5bfd9da1d1b24f326399b6b75558ded14514f23 - -RUN cd /usr/local && git clone https://go.googlesource.com/go go1.14 && cd go1.14 && git reset --hard ${GO_REV} +RUN cd /usr/local && git clone https://go.googlesource.com/go go1.14 && cd go1.14 && git reset --hard $GO_VERSION WORKDIR /usr/local/go1.14/src RUN ./make.bash ENV GOROOT /usr/local/go1.14 From dda036dce9d000910ccadf9d5a8058e882225a67 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Fri, 3 Apr 2020 15:56:17 -0400 Subject: [PATCH 049/148] playground: upgrade to Debian Buster Buster is the latest stable release of Debian. Although Stretch is still covered by LTS, it's better to be up to date. Updates golang/go#25224 Change-Id: If91b7f3de834a6683880a1f661ff8777f58c38e7 Reviewed-on: https://go-review.googlesource.com/c/playground/+/227348 Reviewed-by: Carlos Amedee --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index ef329ea5..ef02200c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,14 @@ # license that can be found in the LICENSE file. ############################################################################ -FROM debian:stretch AS nacl +FROM debian:buster AS nacl RUN apt-get update && apt-get install -y --no-install-recommends curl bzip2 ca-certificates RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 ############################################################################ -FROM debian:stretch AS build +FROM debian:buster AS build LABEL maintainer="golang-dev@googlegroups.com" ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' @@ -82,7 +82,7 @@ RUN ../bin/go install --tags=faketime std ############################################################################ # Final stage. -FROM debian:stretch +FROM debian:buster RUN apt-get update && apt-get install -y git ca-certificates --no-install-recommends From 9d1e738fb7b9362fbfd3055572d94e357364070e Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Fri, 3 Apr 2020 16:00:15 -0400 Subject: [PATCH 050/148] playground: fix the build for gVisor Exec permissions need to be granted on /tmpfs on Docker in order for binaries downloaded there to be executable. Additionally, newer versions of gVisor only create the __runsc_containers__ file in a multi-container environment, which is not the case for testing. Updates golang/go#25224 Change-Id: I272535558d952e423f75ba9770a491472e9c8eff Reviewed-on: https://go-review.googlesource.com/c/playground/+/227349 Reviewed-by: Carlos Amedee --- sandbox/sandbox.go | 2 +- tests.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 41f1f9b8..9b8bcbab 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -358,7 +358,7 @@ func startContainer(ctx context.Context) (c *Container, err error) { cmd := exec.Command("docker", "run", "--name="+name, "--rm", - "--tmpfs=/tmpfs", + "--tmpfs=/tmpfs:exec", "-i", // read stdin "--runtime=runsc", diff --git a/tests.go b/tests.go index 4022b6a3..0f3cda49 100644 --- a/tests.go +++ b/tests.go @@ -264,7 +264,6 @@ func main() { } for _, expect := range []string{ "/.dockerenv", - "/__runsc_containers__", "/etc/hostname", "/dev/zero", "/lib/ld-linux-x86-64.so.2", From 0bc4181d35d0b2624100c1419f7facb915451430 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 8 Apr 2020 12:55:07 -0400 Subject: [PATCH 051/148] playground: correct ARG scoping in Dockerfile ARG is scoped to the build context. In order for it to be global in a Dockerfile, it needs to happen before any FROM statement. Updates golang/go#25224 Change-Id: Iea38d04e36f54a416e21c2e42ac88c03f2db057d Reviewed-on: https://go-review.googlesource.com/c/playground/+/227589 Reviewed-by: Andrew Bonventre Reviewed-by: Carlos Amedee --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index ef02200c..97d9a28d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,8 @@ # Use of this source code is governed by a BSD-style # license that can be found in the LICENSE file. +ARG GO_VERSION=go1.14.1 + ############################################################################ FROM debian:buster AS nacl @@ -20,7 +22,6 @@ ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap ENV GO_BOOTSTRAP_VERSION go1.13.9 -ARG GO_VERSION=go1.14.1 ENV GO_VERSION ${GO_VERSION} # Fake time @@ -74,6 +75,8 @@ FROM golang:1.13 AS temp_pre_go14 ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' RUN apt-get update && apt-get install -y --no-install-recommends ${BUILD_DEPS} +ARG GO_VERSION +ENV GO_VERSION ${GO_VERSION} RUN cd /usr/local && git clone https://go.googlesource.com/go go1.14 && cd go1.14 && git reset --hard $GO_VERSION WORKDIR /usr/local/go1.14/src RUN ./make.bash From 15e968511d3060ae3d38f53ab43cc48529d26825 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 8 Apr 2020 13:52:52 -0400 Subject: [PATCH 052/148] playground: enable gVisor in production The sandbox code checks for GO_VERSION to be set to 1.14+. Setting it correctly in the final stage of the Dockerfile will enable gVisor in production. Tested by using "//play:gvisor" in a unique production snippet. Changes nacl testing path to unset GO_VERSION environment variable, which will trigger the non-gvisor codepath. Updates golang/go#25224 Change-Id: Iaf688fffdb68fcc3db2c6803327f24bbf0f98268 Reviewed-on: https://go-review.googlesource.com/c/playground/+/227643 Reviewed-by: Andrew Bonventre Reviewed-by: Carlos Amedee --- Dockerfile | 3 +++ Makefile | 2 +- tests.go | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 97d9a28d..c3a5c01a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,7 @@ ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap ENV GO_BOOTSTRAP_VERSION go1.13.9 +ARG GO_VERSION ENV GO_VERSION ${GO_VERSION} # Fake time @@ -93,6 +94,8 @@ COPY --from=build /usr/local/go /usr/local/go COPY --from=nacl /tmp/sel_ldr_x86_64 /usr/local/bin COPY --from=temp_pre_go14 /usr/local/go1.14 /usr/local/go1.14 +ARG GO_VERSION +ENV GO_VERSION ${GO_VERSION} ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH diff --git a/Makefile b/Makefile index bac164f3..56ff9307 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ docker: runlocal: docker network create sandnet || true docker kill play_dev || true - docker run --name=play_dev --rm --network=sandnet -ti -p 127.0.0.1:8081:8080/tcp golang/playground + docker run --name=play_dev --rm --network=sandnet --env SANDBOX_BACKEND_URL="http://sandbox_dev.sandnet/run" -ti -p 127.0.0.1:8081:8080/tcp golang/playground test_go: # Run fast tests first: (and tests whether, say, things compile) diff --git a/tests.go b/tests.go index 0f3cda49..454b02a5 100644 --- a/tests.go +++ b/tests.go @@ -29,6 +29,7 @@ type compileTest struct { func (s *server) testNacl() { log.Printf("testing nacl mode") + os.Setenv("GO_VERSION", "") s.runTests() } From cb3741f468aa1d6e77078cb6c38b8fa762713634 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Fri, 3 Apr 2020 16:03:56 -0400 Subject: [PATCH 053/148] playground: use the correct contexts for sandbox requests The sandbox code was incorrectly using the request context instead of the build context when trying to determine if there was a DeadlineExceeded error. This would manifest to the user as a blank response in the build output, rather than the correct error. Additionally, the sandbox code was using the incorrect context for running the binary. This means we were not correctly enforcing maxRunTime. Finally, tests do not pass with a maxRunTime of 2 seconds on my machine, and it's unclear what impact enforcing this would have on production code. I've increased it to 5 seconds. It would be prudent to add metrics here to determine how user programs are impacted in a follow-up issue. Updates golang/go#25224 Updates golang/go#38052 Change-Id: I59aa8caeb63a9eec687bfbe4f69c57f71a13440d Reviewed-on: https://go-review.googlesource.com/c/playground/+/227350 Reviewed-by: Andrew Bonventre Reviewed-by: Bryan C. Mills --- sandbox.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sandbox.go b/sandbox.go index 58431d4b..5fbc89c7 100644 --- a/sandbox.go +++ b/sandbox.go @@ -40,7 +40,7 @@ import ( const ( maxCompileTime = 5 * time.Second - maxRunTime = 2 * time.Second + maxRunTime = 5 * time.Second // progName is the implicit program name written to the temp // dir and used in compiler and vet errors. @@ -420,7 +420,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { cmd.Env = append(cmd.Env, "GOPATH="+goPath) t0 := time.Now() if out, err := cmd.CombinedOutput(); err != nil { - if ctx.Err() == context.DeadlineExceeded { + if buildCtx.Err() == context.DeadlineExceeded { log.Printf("go build timed out after %v", time.Since(t0)) return &response{Errors: goBuildTimeoutError}, nil } @@ -454,7 +454,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { if err != nil { return nil, err } - req, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) + req, err := http.NewRequestWithContext(runCtx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) if err != nil { return nil, err } @@ -487,7 +487,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { cmd.Stdout = rec.Stdout() cmd.Stderr = rec.Stderr() if err := cmd.Run(); err != nil { - if ctx.Err() == context.DeadlineExceeded { + if runCtx.Err() == context.DeadlineExceeded { // Send what was captured before the timeout. events, err := rec.Events() if err != nil { From 27e208bdaf22e4553fb97552f3997ead88b5dacb Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Tue, 31 Mar 2020 14:49:14 -0400 Subject: [PATCH 054/148] playground: drop NaCL support It is longer being used in production as of Go 1.14beta1. Updates golang/go#25224 Change-Id: I5a83c88387f722bb0be476d0500f74a061827770 Reviewed-on: https://go-review.googlesource.com/c/playground/+/227351 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Carlos Amedee --- Dockerfile | 73 +++++----------------- Makefile | 10 +-- main.go | 15 +++-- sandbox.go | 125 +++++++++++++------------------------- sandbox/Dockerfile | 4 +- sandbox/Dockerfile.gvisor | 15 ----- sandbox/sandbox.go | 17 +----- tests.go | 6 -- 8 files changed, 76 insertions(+), 189 deletions(-) diff --git a/Dockerfile b/Dockerfile index c3a5c01a..b402accd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,14 +4,6 @@ ARG GO_VERSION=go1.14.1 -############################################################################ -FROM debian:buster AS nacl - -RUN apt-get update && apt-get install -y --no-install-recommends curl bzip2 ca-certificates - -RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 - -############################################################################ FROM debian:buster AS build LABEL maintainer="golang-dev@googlegroups.com" @@ -20,87 +12,54 @@ RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends ENV GOPATH /go ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH -ENV GOROOT_BOOTSTRAP /usr/local/gobootstrap -ENV GO_BOOTSTRAP_VERSION go1.13.9 +ENV GO_BOOTSTRAP_VERSION go1.14.1 ARG GO_VERSION ENV GO_VERSION ${GO_VERSION} -# Fake time -COPY enable-fake-time.patch /usr/local/playground/ -# Fake file system -COPY fake_fs.lst /usr/local/playground/ - -# Get a bootstrap version of Go for building from source. +# Get a version of Go for building the playground RUN curl -sSL https://dl.google.com/go/$GO_BOOTSTRAP_VERSION.linux-amd64.tar.gz -o /tmp/go.tar.gz RUN curl -sSL https://dl.google.com/go/$GO_BOOTSTRAP_VERSION.linux-amd64.tar.gz.sha256 -o /tmp/go.tar.gz.sha256 RUN echo "$(cat /tmp/go.tar.gz.sha256) /tmp/go.tar.gz" | sha256sum -c - -RUN mkdir -p $GOROOT_BOOTSTRAP -RUN tar --strip=1 -C $GOROOT_BOOTSTRAP -vxzf /tmp/go.tar.gz - -# Fetch Go source for tag $GO_VERSION. -RUN git clone --depth=1 --branch=$GO_BOOTSTRAP_VERSION https://go.googlesource.com/go /usr/local/go -# Apply the fake time and fake filesystem patches. -RUN patch /usr/local/go/src/runtime/rt0_nacl_amd64p32.s /usr/local/playground/enable-fake-time.patch -RUN cd /usr/local/go && $GOROOT_BOOTSTRAP/bin/go run misc/nacl/mkzip.go -p syscall /usr/local/playground/fake_fs.lst src/syscall/fstest_nacl.go -# Build the Go toolchain. -RUN cd /usr/local/go/src && GOOS=nacl GOARCH=amd64p32 ./make.bash --no-clean +RUN mkdir -p /usr/local/go +RUN tar --strip=1 -C /usr/local/go -vxzf /tmp/go.tar.gz RUN mkdir /gocache ENV GOCACHE /gocache ENV GO111MODULE on ENV GOPROXY=https://proxy.golang.org +# Compile Go at target sandbox version and install standard library with --tags=faketime. +WORKDIR /usr/local +RUN git clone https://go.googlesource.com/go go-faketime && cd go-faketime && git reset --hard $GO_VERSION +WORKDIR /usr/local/go-faketime/src +RUN ./make.bash +ENV GOROOT /usr/local/go-faketime +RUN ../bin/go install --tags=faketime std + COPY go.mod /go/src/playground/go.mod COPY go.sum /go/src/playground/go.sum WORKDIR /go/src/playground - -# Pre-build some packages to speed final install later. -RUN go install cloud.google.com/go/compute/metadata -RUN go install cloud.google.com/go/datastore -RUN go install github.com/bradfitz/gomemcache/memcache -RUN go install golang.org/x/tools/godoc/static -RUN go install golang.org/x/tools/imports -RUN go install github.com/rogpeppe/go-internal/modfile -RUN go install github.com/rogpeppe/go-internal/txtar +RUN go mod download # Add and compile playground daemon COPY . /go/src/playground/ -WORKDIR /go/src/playground RUN go install -############################################################################ -# Temporary Docker stage to add a pre-Go1.14 $GOROOT into our -# container for early linux/amd64 testing. -FROM golang:1.13 AS temp_pre_go14 - -ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' -RUN apt-get update && apt-get install -y --no-install-recommends ${BUILD_DEPS} - -ARG GO_VERSION -ENV GO_VERSION ${GO_VERSION} -RUN cd /usr/local && git clone https://go.googlesource.com/go go1.14 && cd go1.14 && git reset --hard $GO_VERSION -WORKDIR /usr/local/go1.14/src -RUN ./make.bash -ENV GOROOT /usr/local/go1.14 -RUN ../bin/go install --tags=faketime std - ############################################################################ # Final stage. FROM debian:buster RUN apt-get update && apt-get install -y git ca-certificates --no-install-recommends -COPY --from=build /usr/local/go /usr/local/go -COPY --from=nacl /tmp/sel_ldr_x86_64 /usr/local/bin -COPY --from=temp_pre_go14 /usr/local/go1.14 /usr/local/go1.14 +COPY --from=build /usr/local/go-faketime /usr/local/go-faketime ARG GO_VERSION ENV GO_VERSION ${GO_VERSION} ENV GOPATH /go -ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH +ENV PATH /usr/local/go-faketime/bin:$GOPATH/bin:$PATH # Add and compile tour packages -RUN GOOS=nacl GOARCH=amd64p32 go get \ +RUN go get \ golang.org/x/tour/pic \ golang.org/x/tour/reader \ golang.org/x/tour/tree \ diff --git a/Makefile b/Makefile index 56ff9307..e96fbd81 100644 --- a/Makefile +++ b/Makefile @@ -12,24 +12,20 @@ docker: runlocal: docker network create sandnet || true docker kill play_dev || true - docker run --name=play_dev --rm --network=sandnet --env SANDBOX_BACKEND_URL="http://sandbox_dev.sandnet/run" -ti -p 127.0.0.1:8081:8080/tcp golang/playground + docker run --name=play_dev --rm --network=sandnet -ti -p 127.0.0.1:8081:8080/tcp golang/playground --backend-url="http://sandbox_dev.sandnet/run" test_go: # Run fast tests first: (and tests whether, say, things compile) GO111MODULE=on go test -v -test_nacl: docker - docker kill sandbox_front_test || true - docker run --rm --name=sandbox_front_test --network=sandnet -t golang/playground testnacl - test_gvisor: docker docker kill sandbox_front_test || true - docker run --rm --name=sandbox_front_test --network=sandnet -t golang/playground test + docker run --rm --name=sandbox_front_test --network=sandnet -t golang/playground --runtests # Note: test_gvisor is not included in "test" yet, because it requires # running a separate server first ("make runlocal" in the sandbox # directory) -test: test_go test_nacl +test: test_go update-cloudbuild-trigger: # The gcloud CLI doesn't yet support updating a trigger. diff --git a/main.go b/main.go index 6b1105e8..733a6a04 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ package main import ( "context" + "flag" "fmt" "net/http" "os" @@ -16,7 +17,13 @@ import ( var log = newStdLogger() +var ( + runtests = flag.Bool("runtests", false, "Run integration tests instead of Playground server.") + backendURL = flag.String("backend-url", "", "URL for sandbox backend that runs Go binaries.") +) + func main() { + flag.Parse() s, err := newServer(func(s *server) error { pid := projectID() if pid == "" { @@ -41,13 +48,13 @@ func main() { log.Fatalf("Error creating server: %v", err) } - if len(os.Args) > 1 && os.Args[1] == "test" { + if *runtests { s.test() return } - if len(os.Args) > 1 && os.Args[1] == "testnacl" { - s.testNacl() - return + if *backendURL != "" { + // TODO(golang.org/issue/25224) - Remove environment variable and use a flag. + os.Setenv("SANDBOX_BACKEND_URL", *backendURL) } port := os.Getenv("PORT") diff --git a/sandbox.go b/sandbox.go index 5fbc89c7..effe8a18 100644 --- a/sandbox.go +++ b/sandbox.go @@ -373,42 +373,22 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { } } - // TODO: simplify this once Go 1.14 is out. We should remove - // the //play:gvisor substring hack and DEBUG_FORCE_GVISOR and - // instead implement https://golang.org/issue/33629 to - // officially support different Go versions (Go tip + past two - // releases). - useGvisor := os.Getenv("GO_VERSION") >= "go1.14" || - os.Getenv("DEBUG_FORCE_GVISOR") == "1" || - strings.Contains(req.Body, "//play:gvisor\n") - exe := filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") buildCtx, cancel := context.WithTimeout(ctx, maxCompileTime) defer cancel() - goBin := "go" - if useGvisor { - goBin = "/usr/local/go1.14/bin/go" - } - cmd := exec.CommandContext(buildCtx, goBin, - "build", - "-o", exe, - "-tags=faketime", // required for Go 1.14+, no-op before - buildPkgArg) + cmd := exec.CommandContext(ctx, "/usr/local/go-faketime/bin/go", "build", "-o", exe, "-tags=faketime", buildPkgArg) cmd.Dir = tmpDir var goPath string - if useGvisor { - cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go1.14"} - } else { - cmd.Env = []string{"GOOS=nacl", "GOARCH=amd64p32"} - } + cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"} cmd.Env = append(cmd.Env, "GOCACHE="+goCache) if useModules { // Create a GOPATH just for modules to be downloaded // into GOPATH/pkg/mod. goPath, err = ioutil.TempDir("", "gopath") if err != nil { + log.Printf("error creating temp directory: %v", err) return nil, fmt.Errorf("error creating temp directory: %v", err) } defer os.RemoveAll(goPath) @@ -438,72 +418,53 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { } return nil, fmt.Errorf("error building go source: %v", err) } - runCtx, cancel := context.WithTimeout(ctx, maxRunTime) - defer cancel() rec := new(Recorder) var exitCode int - if useGvisor { - const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify? - if fi, err := os.Stat(exe); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { - if err != nil { - return nil, fmt.Errorf("failed to stat binary: %v", err) - } - return nil, fmt.Errorf("invalid binary size %d", fi.Size()) - } - exeBytes, err := ioutil.ReadFile(exe) - if err != nil { - return nil, err - } - req, err := http.NewRequestWithContext(runCtx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) + const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify? + if fi, err := os.Stat(exe); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { if err != nil { - return nil, err - } - req.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST - if testParam != "" { - req.Header.Add("X-Argument", testParam) - } - req.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } - res, err := sandboxBackendClient().Do(req) - if err != nil { - return nil, err - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected response from backend: %v", res.Status) - } - var execRes sandboxtypes.Response - if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil { - log.Printf("JSON decode error from backend: %v", err) - return nil, errors.New("error parsing JSON from backend") - } - if execRes.Error != "" { - return &response{Errors: execRes.Error}, nil - } - exitCode = execRes.ExitCode - rec.Stdout().Write(execRes.Stdout) - rec.Stderr().Write(execRes.Stderr) - } else { - cmd := exec.CommandContext(runCtx, "sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", exe, testParam) - cmd.Stdout = rec.Stdout() - cmd.Stderr = rec.Stderr() - if err := cmd.Run(); err != nil { - if runCtx.Err() == context.DeadlineExceeded { - // Send what was captured before the timeout. - events, err := rec.Events() - if err != nil { - return nil, fmt.Errorf("error decoding events: %v", err) - } - return &response{Errors: "process took too long", Events: events}, nil - } - exitErr, ok := err.(*exec.ExitError) - if !ok { - return nil, fmt.Errorf("error running sandbox: %v", err) - } - exitCode = exitErr.ExitCode() + return nil, fmt.Errorf("failed to stat binary: %v", err) } + return nil, fmt.Errorf("invalid binary size %d", fi.Size()) + } + exeBytes, err := ioutil.ReadFile(exe) + if err != nil { + return nil, err + } + runCtx, cancel := context.WithTimeout(ctx, maxRunTime) + defer cancel() + sreq, err := http.NewRequestWithContext(runCtx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) + if err != nil { + return nil, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err) + } + sreq.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST + if testParam != "" { + sreq.Header.Add("X-Argument", testParam) + } + sreq.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } + res, err := sandboxBackendClient().Do(sreq) + if err != nil { + return nil, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + log.Printf("unexpected response from backend: %v", res.Status) + return nil, fmt.Errorf("unexpected response from backend: %v", res.Status) + } + var execRes sandboxtypes.Response + if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil { + log.Printf("JSON decode error from backend: %v", err) + return nil, errors.New("error parsing JSON from backend") + } + if execRes.Error != "" { + return &response{Errors: execRes.Error}, nil } + exitCode = execRes.ExitCode + rec.Stdout().Write(execRes.Stdout) + rec.Stderr().Write(execRes.Stderr) events, err := rec.Events() if err != nil { + log.Printf("error decoding events: %v", err) return nil, fmt.Errorf("error decoding events: %v", err) } var fails int diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index ceb5401a..f2da1d59 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -4,7 +4,7 @@ # environment so the play-sandbox server can connect to the host's # docker daemon, which has the gvisor "runsc" runtime available. -FROM golang:1.13 AS build +FROM golang:1.14 AS build COPY . /go/src/playground WORKDIR /go/src/playground/sandbox @@ -15,7 +15,7 @@ FROM debian:buster RUN apt-get update # Extra stuff for occasional debugging: -RUN apt-get install --yes strace lsof emacs25-nox net-tools tcpdump procps +RUN apt-get install --yes strace lsof emacs-nox net-tools tcpdump procps # Install Docker CLI: RUN apt-get install --yes \ diff --git a/sandbox/Dockerfile.gvisor b/sandbox/Dockerfile.gvisor index 42188871..464aaec7 100644 --- a/sandbox/Dockerfile.gvisor +++ b/sandbox/Dockerfile.gvisor @@ -8,17 +8,6 @@ # gvisor-contained helper. FROM golang/playground-sandbox AS server -############################################################################ -# Temporary nacl compatibility for development & incremental -# deployment purposes, so we can run the new server architecture in -# nacl mode for a bit, then opt-in linux/amd64 gvisor mode, and -# then once Go 1.14 is out for real we remove the nacl option and -# delete all the nacl code. -FROM debian:buster AS nacl -RUN apt-get update && apt-get install -y --no-install-recommends curl bzip2 ca-certificates -RUN curl -s https://storage.googleapis.com/nativeclient-mirror/nacl/nacl_sdk/trunk.544461/naclsdk_linux.tar.bz2 | tar -xj -C /tmp --strip-components=2 pepper_67/tools/sel_ldr_x86_64 - - ############################################################################ # This is the actual environment things run in: a minimal busybox with glibc # binaries so we can use cgo. @@ -26,8 +15,4 @@ FROM busybox:glibc COPY --from=server /usr/local/bin/play-sandbox /usr/local/bin/play-sandbox -# And this, temporarily, for being able to test the old nacl binaries -# with the new sandbox: -COPY --from=nacl /tmp/sel_ldr_x86_64 /usr/local/bin - ENTRYPOINT ["/usr/local/bin/play-sandbox"] diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 9b8bcbab..603088b8 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -255,27 +255,12 @@ func runInGvisor() { log.Fatalf("writing header to stderr: %v", err) } - // As part of a temporary transition plan, we also support - // running nacl binaries in this sandbox. The point isn't to - // double sandbox things as much as it is to let us transition - // things in steps: first to split the sandbox into two parts - // (frontend & backend), and then to change the type of binary - // (nacl to linux/amd64). This means we can do step 1 of the - // migration during the Go 1.13 dev cycle and have less - // risk/rush during the Go 1.14 release, which should just be - // a flag flip. - // This isn't a perfect heuristic, but it works and it's cheap: - isNacl := bytes.Contains(slurp, []byte("_rt0_amd64p32_nacl")) - cmd := exec.Command(binPath) - if isNacl { - cmd = exec.Command("/usr/local/bin/sel_ldr_x86_64", "-l", "/dev/null", "-S", "-e", binPath) - } cmd.Args = append(cmd.Args, meta.Args...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { - log.Fatal(err) + log.Fatalf("cmd.Start(): %v", err) } err = cmd.Wait() os.Remove(binPath) // not that it matters much, this container will be nuked diff --git a/tests.go b/tests.go index 454b02a5..9e0d55fb 100644 --- a/tests.go +++ b/tests.go @@ -27,12 +27,6 @@ type compileTest struct { wantVetErrors string } -func (s *server) testNacl() { - log.Printf("testing nacl mode") - os.Setenv("GO_VERSION", "") - s.runTests() -} - func (s *server) test() { if _, err := net.ResolveIPAddr("ip", "sandbox_dev.sandnet."); err != nil { log.Fatalf("sandbox_dev.sandnet not available") From 688d9f61624f5734aad470dece2704686c4a6632 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Mon, 6 Apr 2020 14:46:29 -0400 Subject: [PATCH 055/148] playground: move build and run into functions Based on a suggestion in CL 226697, this change moves the build and run steps from compileAndRun into their own functions. This will make it less likely to accidentally use the incorrect context again, which was the cause of golang/go#38052. Updates golang/go#25224 Change-Id: Id8399c2bd93fca7d61dced0431c8596f7998f3ee Reviewed-on: https://go-review.googlesource.com/c/playground/+/227352 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Bryan C. Mills Reviewed-by: Carlos Amedee --- sandbox.go | 182 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 67 deletions(-) diff --git a/sandbox.go b/sandbox.go index effe8a18..3f44a09c 100644 --- a/sandbox.go +++ b/sandbox.go @@ -328,24 +328,95 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { } defer os.RemoveAll(tmpDir) - files, err := splitFiles([]byte(req.Body)) + br, err := sandboxBuild(ctx, tmpDir, []byte(req.Body), req.WithVet) if err != nil { - return &response{Errors: err.Error()}, nil + return nil, err + } + if br.errorMessage != "" { + return &response{Errors: br.errorMessage}, nil + } + + execRes, err := sandboxRun(ctx, br.exePath, br.testParam) + if err != nil { + return nil, err + } + if execRes.Error != "" { + return &response{Errors: execRes.Error}, nil + } + + rec := new(Recorder) + rec.Stdout().Write(execRes.Stdout) + rec.Stderr().Write(execRes.Stderr) + events, err := rec.Events() + if err != nil { + log.Printf("error decoding events: %v", err) + return nil, fmt.Errorf("error decoding events: %v", err) + } + var fails int + if br.testParam != "" { + // In case of testing the TestsFailed field contains how many tests have failed. + for _, e := range events { + fails += strings.Count(e.Message, failedTestPattern) + } + } + return &response{ + Events: events, + Status: execRes.ExitCode, + IsTest: br.testParam != "", + TestsFailed: fails, + VetErrors: br.vetOut, + VetOK: req.WithVet && br.vetOut == "", + }, nil +} + +// buildResult is the output of a sandbox build attempt. +type buildResult struct { + // goPath is a temporary directory if the binary was built with module support. + // TODO(golang.org/issue/25224) - Why is the module mode built so differently? + goPath string + // exePath is the path to the built binary. + exePath string + // useModules is true if the binary was built with module support. + useModules bool + // testParam is set if tests should be run when running the binary. + testParam string + // errorMessage is an error message string to be returned to the user. + errorMessage string + // vetOut is the output of go vet, if requested. + vetOut string +} + +// cleanup cleans up the temporary goPath created when building with module support. +func (b *buildResult) cleanup() error { + if b.useModules && b.goPath != "" { + return os.RemoveAll(b.goPath) + } + return nil +} + +// sandboxBuild builds a Go program and returns a build result that includes the build context. +// +// An error is returned if a non-user-correctable error has occurred. +func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (*buildResult, error) { + files, err := splitFiles(in) + if err != nil { + return &buildResult{errorMessage: err.Error()}, nil } - var testParam string + br := new(buildResult) + defer br.cleanup() var buildPkgArg = "." if files.Num() == 1 && len(files.Data(progName)) > 0 { buildPkgArg = progName src := files.Data(progName) if code := getTestProg(src); code != nil { - testParam = "-test.v" + br.testParam = "-test.v" files.AddFile(progName, code) } } - useModules := allowModuleDownloads(files) - if !files.Contains("go.mod") && useModules { + br.useModules = allowModuleDownloads(files) + if !files.Contains("go.mod") && br.useModules { files.AddFile("go.mod", []byte("module play\n")) } @@ -358,7 +429,7 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { fset := token.NewFileSet() f, err := parser.ParseFile(fset, f, src, parser.PackageClauseOnly) if err == nil && f.Name.Name != "main" { - return &response{Errors: "package name must be main"}, nil + return &buildResult{errorMessage: "package name must be main"}, nil } } @@ -373,69 +444,80 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { } } - exe := filepath.Join(tmpDir, "a.out") + br.exePath = filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") - buildCtx, cancel := context.WithTimeout(ctx, maxCompileTime) + ctx, cancel := context.WithTimeout(ctx, maxCompileTime) defer cancel() - cmd := exec.CommandContext(ctx, "/usr/local/go-faketime/bin/go", "build", "-o", exe, "-tags=faketime", buildPkgArg) + cmd := exec.CommandContext(ctx, "/usr/local/go-faketime/bin/go", "build", "-o", br.exePath, "-tags=faketime") cmd.Dir = tmpDir - var goPath string cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"} cmd.Env = append(cmd.Env, "GOCACHE="+goCache) - if useModules { + if br.useModules { // Create a GOPATH just for modules to be downloaded // into GOPATH/pkg/mod. - goPath, err = ioutil.TempDir("", "gopath") + cmd.Args = append(cmd.Args, "-modcacherw") + br.goPath, err = ioutil.TempDir("", "gopath") if err != nil { log.Printf("error creating temp directory: %v", err) return nil, fmt.Errorf("error creating temp directory: %v", err) } - defer os.RemoveAll(goPath) cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY="+playgroundGoproxy()) } else { - goPath = os.Getenv("GOPATH") // contains old code.google.com/p/go-tour, etc + br.goPath = os.Getenv("GOPATH") // contains old code.google.com/p/go-tour, etc cmd.Env = append(cmd.Env, "GO111MODULE=off") // in case it becomes on by default later } - cmd.Env = append(cmd.Env, "GOPATH="+goPath) + cmd.Args = append(cmd.Args, buildPkgArg) + cmd.Env = append(cmd.Env, "GOPATH="+br.goPath) t0 := time.Now() if out, err := cmd.CombinedOutput(); err != nil { - if buildCtx.Err() == context.DeadlineExceeded { + if ctx.Err() == context.DeadlineExceeded { log.Printf("go build timed out after %v", time.Since(t0)) - return &response{Errors: goBuildTimeoutError}, nil + return &buildResult{errorMessage: goBuildTimeoutError}, nil } if _, ok := err.(*exec.ExitError); ok { // Return compile errors to the user. // Rewrite compiler errors to strip the tmpDir name. - errs := strings.Replace(string(out), tmpDir+"/", "", -1) + br.errorMessage = strings.Replace(string(out), tmpDir+"/", "", -1) // "go build", invoked with a file name, puts this odd // message before any compile errors; strip it. - errs = strings.Replace(errs, "# command-line-arguments\n", "", 1) + br.errorMessage = strings.Replace(br.errorMessage, "# command-line-arguments\n", "", 1) - return &response{Errors: errs}, nil + return br, nil } return nil, fmt.Errorf("error building go source: %v", err) } - rec := new(Recorder) - var exitCode int const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify? - if fi, err := os.Stat(exe); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { + if fi, err := os.Stat(br.exePath); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { if err != nil { return nil, fmt.Errorf("failed to stat binary: %v", err) } return nil, fmt.Errorf("invalid binary size %d", fi.Size()) } - exeBytes, err := ioutil.ReadFile(exe) + if vet { + // TODO: do this concurrently with the execution to reduce latency. + br.vetOut, err = vetCheckInDir(tmpDir, br.goPath, br.useModules) + if err != nil { + return nil, fmt.Errorf("running vet: %v", err) + } + } + return br, nil +} + +// sandboxRun runs a Go binary in a sandbox environment. +func sandboxRun(ctx context.Context, exePath string, testParam string) (sandboxtypes.Response, error) { + var execRes sandboxtypes.Response + exeBytes, err := ioutil.ReadFile(exePath) if err != nil { - return nil, err + return execRes, err } - runCtx, cancel := context.WithTimeout(ctx, maxRunTime) + ctx, cancel := context.WithTimeout(ctx, maxRunTime) defer cancel() - sreq, err := http.NewRequestWithContext(runCtx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) + sreq, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes)) if err != nil { - return nil, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err) + return execRes, fmt.Errorf("NewRequestWithContext %q: %w", sandboxBackendURL(), err) } sreq.Header.Add("Idempotency-Key", "1") // lets Transport do retries with a POST if testParam != "" { @@ -444,52 +526,18 @@ func compileAndRun(ctx context.Context, req *request) (*response, error) { sreq.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } res, err := sandboxBackendClient().Do(sreq) if err != nil { - return nil, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err) + return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { log.Printf("unexpected response from backend: %v", res.Status) - return nil, fmt.Errorf("unexpected response from backend: %v", res.Status) + return execRes, fmt.Errorf("unexpected response from backend: %v", res.Status) } - var execRes sandboxtypes.Response if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil { log.Printf("JSON decode error from backend: %v", err) - return nil, errors.New("error parsing JSON from backend") - } - if execRes.Error != "" { - return &response{Errors: execRes.Error}, nil - } - exitCode = execRes.ExitCode - rec.Stdout().Write(execRes.Stdout) - rec.Stderr().Write(execRes.Stderr) - events, err := rec.Events() - if err != nil { - log.Printf("error decoding events: %v", err) - return nil, fmt.Errorf("error decoding events: %v", err) - } - var fails int - if testParam != "" { - // In case of testing the TestsFailed field contains how many tests have failed. - for _, e := range events { - fails += strings.Count(e.Message, failedTestPattern) - } + return execRes, errors.New("error parsing JSON from backend") } - var vetOut string - if req.WithVet { - // TODO: do this concurrently with the execution to reduce latency. - vetOut, err = vetCheckInDir(tmpDir, goPath, useModules) - if err != nil { - return nil, fmt.Errorf("running vet: %v", err) - } - } - return &response{ - Events: events, - Status: exitCode, - IsTest: testParam != "", - TestsFailed: fails, - VetErrors: vetOut, - VetOK: req.WithVet && vetOut == "", - }, nil + return execRes, nil } // allowModuleDownloads reports whether the code snippet in src should be allowed From 8e01013ecfc05c0d2a96c3d24a0dc1cee6062714 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Tue, 14 Apr 2020 19:15:54 -0400 Subject: [PATCH 056/148] playground: isolate playground build stage in Docker This ensures that our build environment is isolated from the faketime build environment. Updates golang/go#25224 Change-Id: I7ad483f34483bc00ef097e8b92ce45d8936f47f1 Reviewed-on: https://go-review.googlesource.com/c/playground/+/228262 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov --- Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index b402accd..5a314c16 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ARG GO_VERSION=go1.14.1 -FROM debian:buster AS build +FROM debian:buster AS go-faketime LABEL maintainer="golang-dev@googlegroups.com" ENV BUILD_DEPS 'curl git gcc patch libc6-dev ca-certificates' @@ -36,6 +36,8 @@ RUN ./make.bash ENV GOROOT /usr/local/go-faketime RUN ../bin/go install --tags=faketime std +FROM golang:1.14 as build-playground + COPY go.mod /go/src/playground/go.mod COPY go.sum /go/src/playground/go.sum WORKDIR /go/src/playground @@ -51,7 +53,7 @@ FROM debian:buster RUN apt-get update && apt-get install -y git ca-certificates --no-install-recommends -COPY --from=build /usr/local/go-faketime /usr/local/go-faketime +COPY --from=go-faketime /usr/local/go-faketime /usr/local/go-faketime ARG GO_VERSION ENV GO_VERSION ${GO_VERSION} @@ -80,7 +82,7 @@ RUN mkdir -p $GOPATH/src/code.google.com/p/go-tour && \ RUN mkdir /app -COPY --from=build /go/bin/playground /app +COPY --from=build-playground /go/bin/playground /app COPY edit.html /app COPY static /app/static COPY examples /app/examples From 0a10c623befa45d2926d68ea4b036a3840794fe7 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 8 Apr 2020 15:54:08 -0400 Subject: [PATCH 057/148] internal/gcpdial: throttle polling for instances gcpdial polls for instances in case instances from the sandbox instance group change. Unfortunately, it is causing us to go over quota in our production project, which breaks other services (mainly deploying from Cloud Build). This change introduces a 10s ticker. Verified locally using internal/gcpdial/gcpdialtool. Updates golang/go#38315 Change-Id: I36009397233c6fc1663a7f2cee4f47caea1ca161 Reviewed-on: https://go-review.googlesource.com/c/playground/+/227648 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov Reviewed-by: Carlos Amedee --- internal/gcpdial/gcpdial.go | 80 +++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/internal/gcpdial/gcpdial.go b/internal/gcpdial/gcpdial.go index 212e1496..9fe3661b 100644 --- a/internal/gcpdial/gcpdial.go +++ b/internal/gcpdial/gcpdial.go @@ -18,7 +18,7 @@ import ( "sync" "time" - compute "google.golang.org/api/compute/v1" + "google.golang.org/api/compute/v1" ) type Dialer struct { @@ -188,46 +188,58 @@ func (d *Dialer) pickIP() (string, bool) { } func (d *Dialer) poll() { + // TODO(golang.org/issue/38315) - Plumb a context in here correctly + ctx := context.TODO() + t := time.NewTicker(10 * time.Second) + defer t.Stop() for { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - res, err := d.lister.ListInstances(ctx) - cancel() - if err != nil { - log.Printf("gcpdial: polling %v: %v", d.lister, err) - time.Sleep(10 * time.Second) - continue + d.pollOnce(ctx) + select { + case <-ctx.Done(): + return + case <-t.C: } + } +} - want := map[string]bool{} // the res []string turned into a set - for _, instURL := range res { - want[instURL] = true - } +func (d *Dialer) pollOnce(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, 30*time.Second) + res, err := d.lister.ListInstances(ctx) + cancel() + if err != nil { + log.Printf("gcpdial: polling %v: %v", d.lister, err) + return + } + + want := map[string]bool{} // the res []string turned into a set + for _, instURL := range res { + want[instURL] = true + } - d.mu.Lock() - // Stop + remove any health check probers that no longer appear in the - // instance group. - for instURL, prober := range d.prober { - if !want[instURL] { - prober.cancel() - delete(d.prober, instURL) - } + d.mu.Lock() + defer d.mu.Unlock() + // Stop + remove any health check probers that no longer appear in the + // instance group. + for instURL, prober := range d.prober { + if !want[instURL] { + prober.cancel() + delete(d.prober, instURL) + } + } + // And start any new health check probers that are newly added + // (or newly known at least) to the instance group. + for _, instURL := range res { + if _, ok := d.prober[instURL]; ok { + continue } - // And start any new health check probers that are newly added - // (or newly known at least) to the instance group. - for _, instURL := range res { - if _, ok := d.prober[instURL]; ok { - continue - } - p := newProber(d, instURL) - go p.probeLoop() - if d.prober == nil { - d.prober = map[string]*prober{} - } - d.prober[instURL] = p + p := newProber(d, instURL) + go p.probeLoop() + if d.prober == nil { + d.prober = map[string]*prober{} } - d.lastInstances = res - d.mu.Unlock() + d.prober[instURL] = p } + d.lastInstances = res } // NewRegionInstanceGroupDialer returns a new dialer that dials named From 136c41e8f22c59b5bc1d94d6ee41abac31b5b5cc Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 8 Apr 2020 18:10:00 -0400 Subject: [PATCH 058/148] sandbox: timeout runsc commands The current mechanism for forcing a process to die ater a timeout is not sufficient. This change fixes issues that were causing processes to run forever on the sandbox. - Gracefully terminate processes before we kill them inside of our gVisor process. This helps capture valuable debug output for the user. - Return a friendlier error when our run context times out on the playground. - Add a test that timeouts are handled gracefully. - Reduce concurrent goroutines in our sandbox run handler by replacing goroutine copy functions with a custom writer (limitedWriter) that returns an error if too much output is returned, halting the program. - Custom writers (limitedWriter, switchWriter) also fix timing errors when calling Wait() too soon on a Command, before we have read all of the data. It also fixes a different error from trying to read data after a program has terminated. - Remove goroutine from startContainer, and use a ticker + context timeout for synchronization. Updates golang/go#25224 Updates golang/go#38343 Change-Id: Ie9d65220e5c4f39272ea70b45c4b472bcd7069bb Reviewed-on: https://go-review.googlesource.com/c/playground/+/227652 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Bryan C. Mills --- Makefile | 2 +- sandbox.go | 4 + sandbox/sandbox.go | 296 ++++++++++++++++++++++++---------------- sandbox/sandbox_test.go | 180 ++++++++++++++++++++++++ tests.go | 24 +++- 5 files changed, 388 insertions(+), 118 deletions(-) create mode 100644 sandbox/sandbox_test.go diff --git a/Makefile b/Makefile index e96fbd81..e670713b 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ runlocal: test_go: # Run fast tests first: (and tests whether, say, things compile) - GO111MODULE=on go test -v + GO111MODULE=on go test -v ./... test_gvisor: docker docker kill sandbox_front_test || true diff --git a/sandbox.go b/sandbox.go index 3f44a09c..07a0f07e 100644 --- a/sandbox.go +++ b/sandbox.go @@ -526,6 +526,10 @@ func sandboxRun(ctx context.Context, exePath string, testParam string) (sandboxt sreq.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } res, err := sandboxBackendClient().Do(sreq) if err != nil { + if ctx.Err() == context.DeadlineExceeded { + execRes.Error = "timeout running program" + return execRes, nil + } return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err) } defer res.Body.Close() diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 603088b8..07de2867 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -43,12 +43,16 @@ var ( const ( maxBinarySize = 100 << 20 + startTimeout = 30 * time.Second runTimeout = 5 * time.Second maxOutputSize = 100 << 20 memoryLimitBytes = 100 << 20 ) -var errTooMuchOutput = errors.New("Output too large") +var ( + errTooMuchOutput = errors.New("Output too large") + errRunTimeout = errors.New("timeout running program") +) // containedStartMessage is the first thing written to stdout by the // gvisor-contained process when it starts up. This lets the parent HTTP @@ -68,8 +72,8 @@ var ( type Container struct { name string stdin io.WriteCloser - stdout io.ReadCloser - stderr io.ReadCloser + stdout *limitedWriter + stderr *limitedWriter cmd *exec.Cmd waitOnce sync.Once @@ -78,12 +82,12 @@ type Container struct { func (c *Container) Close() { setContainerWanted(c.name, false) - c.stdin.Close() - c.stdout.Close() - c.stderr.Close() + if c.cmd.Process != nil { - c.cmd.Process.Kill() - c.Wait() // just in case + gracefulStop(c.cmd.Process, 250*time.Millisecond) + if err := c.Wait(); err != nil { + log.Printf("error in c.Wait() for %q: %v", c.name, err) + } } } @@ -245,6 +249,7 @@ func runInGvisor() { if err := ioutil.WriteFile(binPath, bin, 0755); err != nil { log.Fatalf("writing contained binary: %v", err) } + defer os.Remove(binPath) // not that it matters much, this container will be nuked var meta processMeta if err := json.NewDecoder(bytes.NewReader(metaJSON)).Decode(&meta); err != nil { @@ -262,12 +267,34 @@ func runInGvisor() { if err := cmd.Start(); err != nil { log.Fatalf("cmd.Start(): %v", err) } + timer := time.AfterFunc(runTimeout-(500*time.Millisecond), func() { + fmt.Fprintln(os.Stderr, "timeout running program") + gracefulStop(cmd.Process, 250*time.Millisecond) + }) + defer timer.Stop() err = cmd.Wait() - os.Remove(binPath) // not that it matters much, this container will be nuked os.Exit(errExitCode(err)) return } +// gracefulStop attempts to send a SIGINT before a SIGKILL. +// +// The process will be sent a SIGINT immediately. If the context has still not been cancelled, +// the process will be sent a SIGKILL after delay has passed since sending the SIGINT. +// +// TODO(golang.org/issue/38343) - Change SIGINT to SIGQUIT once decision is made. +func gracefulStop(p *os.Process, delay time.Duration) { + // TODO(golang.org/issue/38343) - Change to syscall.SIGQUIT once decision is made. + if err := p.Signal(os.Interrupt); err != nil { + log.Printf("cmd.Process.Signal(%v): %v", os.Interrupt, err) + } + time.AfterFunc(delay, func() { + if err := p.Kill(); err != nil { + log.Printf("cmd.Process.Kill(): %v", err) + } + }) +} + func makeWorkers() { for { c, err := startContainer(context.Background()) @@ -321,25 +348,6 @@ func getContainer(ctx context.Context) (*Container, error) { func startContainer(ctx context.Context) (c *Container, err error) { name := "play_run_" + randHex(8) setContainerWanted(name, true) - var stdin io.WriteCloser - var stdout io.ReadCloser - var stderr io.ReadCloser - defer func() { - if err == nil { - return - } - setContainerWanted(name, false) - if stdin != nil { - stdin.Close() - } - if stdout != nil { - stdout.Close() - } - if stderr != nil { - stderr.Close() - } - }() - cmd := exec.Command("docker", "run", "--name="+name, "--rm", @@ -352,46 +360,53 @@ func startContainer(ctx context.Context) (c *Container, err error) { *container, "--mode=contained") - stdin, err = cmd.StdinPipe() - if err != nil { - return nil, err - } - stdout, err = cmd.StdoutPipe() - if err != nil { - return nil, err - } - stderr, err = cmd.StderrPipe() + stdin, err := cmd.StdinPipe() if err != nil { return nil, err } + pr, pw := io.Pipe() + stdout := &limitedWriter{dst: &bytes.Buffer{}, n: maxOutputSize + int64(len(containedStartMessage))} + stderr := &limitedWriter{dst: &bytes.Buffer{}, n: maxOutputSize} + cmd.Stdout = &switchWriter{switchAfter: []byte(containedStartMessage), dst1: pw, dst2: stdout} + cmd.Stderr = stderr if err := cmd.Start(); err != nil { return nil, err } + defer func() { + if err != nil { + log.Printf("error starting container %q: %v", name, err) + gracefulStop(cmd.Process, 250*time.Millisecond) + setContainerWanted(name, false) + } + }() + ctx, cancel := context.WithTimeout(ctx, startTimeout) + defer cancel() - errc := make(chan error, 1) + startErr := make(chan error, 1) go func() { buf := make([]byte, len(containedStartMessage)) - if _, err := io.ReadFull(stdout, buf); err != nil { - errc <- fmt.Errorf("error reading header from sandbox container: %v", err) - return - } - if string(buf) != containedStartMessage { - errc <- fmt.Errorf("sandbox container sent wrong header %q; want %q", buf, containedStartMessage) - return + _, err := io.ReadFull(pr, buf) + if err != nil { + startErr <- fmt.Errorf("error reading header from sandbox container: %v", err) + } else if string(buf) != containedStartMessage { + startErr <- fmt.Errorf("sandbox container sent wrong header %q; want %q", buf, containedStartMessage) + } else { + startErr <- nil } - errc <- nil }() + select { case <-ctx.Done(): - log.Printf("timeout starting container") - cmd.Process.Kill() - return nil, ctx.Err() - case err := <-errc: + err := fmt.Errorf("timeout starting container %q: %w", name, ctx.Err()) + pw.Close() + <-startErr + return nil, err + case err = <-startErr: if err != nil { - log.Printf("error starting container: %v", err) return nil, err } } + log.Printf("started container %q", name) return &Container{ name: name, stdin: stdin, @@ -435,6 +450,7 @@ func runHandler(w http.ResponseWriter, r *http.Request) { bin, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, maxBinarySize)) if err != nil { + log.Printf("failed to read request body: %v", err) http.Error(w, err.Error(), http.StatusInternalServerError) return } @@ -451,88 +467,136 @@ func runHandler(w http.ResponseWriter, r *http.Request) { return } logf("got container %s", c.name) - defer c.Close() - defer logf("leaving handler; about to close container") - - runTimer := time.NewTimer(runTimeout) - defer runTimer.Stop() - - errc := make(chan error, 2) // user-visible error - waitc := make(chan error, 1) - - copyOut := func(which string, dst *[]byte, r io.Reader) { - buf := make([]byte, 4<<10) - for { - n, err := r.Read(buf) - logf("%s: Read = %v, %v", which, n, err) - *dst = append(*dst, buf[:n]...) - if err == io.EOF { - return - } - if len(*dst) > maxOutputSize { - errc <- errTooMuchOutput - return - } - if err != nil { - log.Printf("reading %s: %v", which, err) - errc <- fmt.Errorf("error reading %v", which) - return - } - } - } - res := &sandboxtypes.Response{} + ctx, cancel := context.WithTimeout(context.Background(), runTimeout) + closed := make(chan struct{}) + defer func() { + logf("leaving handler; about to close container") + cancel() + <-closed + }() go func() { - var meta processMeta - meta.Args = r.Header["X-Argument"] - metaJSON, _ := json.Marshal(&meta) - metaJSON = append(metaJSON, '\n') - if _, err := c.stdin.Write(metaJSON); err != nil { - log.Printf("stdin write meta: %v", err) - errc <- errors.New("failed to write meta to child") - return + <-ctx.Done() + if ctx.Err() == context.DeadlineExceeded { + logf("timeout") } - if _, err := c.stdin.Write(bin); err != nil { - log.Printf("stdin write: %v", err) - errc <- errors.New("failed to write binary to child") - return - } - c.stdin.Close() - logf("wrote+closed") - go copyOut("stdout", &res.Stdout, c.stdout) - go copyOut("stderr", &res.Stderr, c.stderr) - waitc <- c.Wait() + c.Close() + close(closed) }() - var waitErr error + var meta processMeta + meta.Args = r.Header["X-Argument"] + metaJSON, _ := json.Marshal(&meta) + metaJSON = append(metaJSON, '\n') + if _, err := c.stdin.Write(metaJSON); err != nil { + log.Printf("failed to write meta to child: %v", err) + http.Error(w, "unknown error during docker run", http.StatusInternalServerError) + return + } + if _, err := c.stdin.Write(bin); err != nil { + log.Printf("failed to write binary to child: %v", err) + http.Error(w, "unknown error during docker run", http.StatusInternalServerError) + return + } + c.stdin.Close() + logf("wrote+closed") + err = c.Wait() select { - case waitErr = <-waitc: - logf("waited: %v", waitErr) - case err := <-errc: - logf("got error: %v", err) - if err == errTooMuchOutput { - sendError(w, err.Error()) + case <-ctx.Done(): + // Timed out or canceled before or exactly as Wait returned. + // Either way, treat it as a timeout. + sendError(w, "timeout running program") + return + default: + logf("finished running; about to close container") + cancel() + } + res := &sandboxtypes.Response{} + if err != nil { + if c.stderr.n < 0 || c.stdout.n < 0 { + // Do not send truncated output, just send the error. + sendError(w, errTooMuchOutput.Error()) return } - if err != nil { - http.Error(w, "failed to read stdout from docker run", http.StatusInternalServerError) + var ee *exec.ExitError + if !errors.As(err, &ee) { + http.Error(w, "unknown error during docker run", http.StatusInternalServerError) return } - case <-runTimer.C: - logf("timeout") - sendError(w, "timeout running program") - return + res.ExitCode = ee.ExitCode() } - - res.ExitCode = errExitCode(waitErr) - res.Stderr = cleanStderr(res.Stderr) + res.Stdout = c.stdout.dst.Bytes() + res.Stderr = cleanStderr(c.stderr.dst.Bytes()) sendResponse(w, res) } +// limitedWriter is an io.Writer that returns an errTooMuchOutput when the cap (n) is hit. +type limitedWriter struct { + dst *bytes.Buffer + n int64 // max bytes remaining +} + +// Write is an io.Writer function that returns errTooMuchOutput when the cap (n) is hit. +// +// Partial data will be written to dst if p is larger than n, but errTooMuchOutput will be returned. +func (l *limitedWriter) Write(p []byte) (int, error) { + defer func() { l.n -= int64(len(p)) }() + + if l.n <= 0 { + return 0, errTooMuchOutput + } + + if int64(len(p)) > l.n { + n, err := l.dst.Write(p[:l.n]) + if err != nil { + return n, err + } + return n, errTooMuchOutput + } + + return l.dst.Write(p) +} + +// switchWriter writes to dst1 until switchAfter is written, the it writes to dst2. +type switchWriter struct { + dst1 io.Writer + dst2 io.Writer + switchAfter []byte + buf []byte + found bool +} + +func (s *switchWriter) Write(p []byte) (int, error) { + if s.found { + return s.dst2.Write(p) + } + + s.buf = append(s.buf, p...) + i := bytes.Index(s.buf, s.switchAfter) + if i == -1 { + if len(s.buf) >= len(s.switchAfter) { + s.buf = s.buf[len(s.buf)-len(s.switchAfter)+1:] + } + return s.dst1.Write(p) + } + + s.found = true + nAfter := len(s.buf) - (i + len(s.switchAfter)) + s.buf = nil + + n, err := s.dst1.Write(p[:len(p)-nAfter]) + if err != nil { + return n, err + } + n2, err := s.dst2.Write(p[len(p)-nAfter:]) + return n + n2, err +} + func errExitCode(err error) int { if err == nil { return 0 } - if ee, ok := err.(*exec.ExitError); ok { + var ee *exec.ExitError + if errors.As(err, &ee) { return ee.ExitCode() } return 1 diff --git a/sandbox/sandbox_test.go b/sandbox/sandbox_test.go new file mode 100644 index 00000000..7b32ec6e --- /dev/null +++ b/sandbox/sandbox_test.go @@ -0,0 +1,180 @@ +package main + +import ( + "bytes" + "io" + "strings" + "testing" + "testing/iotest" +) + +func TestLimitedWriter(t *testing.T) { + cases := []struct { + desc string + lw *limitedWriter + in []byte + want []byte + wantN int64 + wantRemaining int64 + err error + }{ + { + desc: "simple", + lw: &limitedWriter{dst: &bytes.Buffer{}, n: 10}, + in: []byte("hi"), + want: []byte("hi"), + wantN: 2, + wantRemaining: 8, + }, + { + desc: "writing nothing", + lw: &limitedWriter{dst: &bytes.Buffer{}, n: 10}, + in: []byte(""), + want: []byte(""), + wantN: 0, + wantRemaining: 10, + }, + { + desc: "writing exactly enough", + lw: &limitedWriter{dst: &bytes.Buffer{}, n: 6}, + in: []byte("enough"), + want: []byte("enough"), + wantN: 6, + wantRemaining: 0, + err: nil, + }, + { + desc: "writing too much", + lw: &limitedWriter{dst: &bytes.Buffer{}, n: 10}, + in: []byte("this is much longer than 10"), + want: []byte("this is mu"), + wantN: 10, + wantRemaining: -1, + err: errTooMuchOutput, + }, + } + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + n, err := io.Copy(c.lw, iotest.OneByteReader(bytes.NewReader(c.in))) + if err != c.err { + t.Errorf("c.lw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) + } + if n != c.wantN { + t.Errorf("c.lw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) + } + if c.lw.n != c.wantRemaining { + t.Errorf("c.lw.n = %d, wanted %d", c.lw.n, c.wantRemaining) + } + if string(c.lw.dst.Bytes()) != string(c.want) { + t.Errorf("c.lw.dst.Bytes() = %q, wanted %q", c.lw.dst.Bytes(), c.want) + } + }) + } +} + +func TestSwitchWriter(t *testing.T) { + cases := []struct { + desc string + sw *switchWriter + in []byte + want1 []byte + want2 []byte + wantN int64 + wantFound bool + err error + }{ + { + desc: "not found", + sw: &switchWriter{switchAfter: []byte("UNIQUE")}, + in: []byte("hi"), + want1: []byte("hi"), + want2: []byte(""), + wantN: 2, + wantFound: false, + }, + { + desc: "writing nothing", + sw: &switchWriter{switchAfter: []byte("UNIQUE")}, + in: []byte(""), + want1: []byte(""), + want2: []byte(""), + wantN: 0, + wantFound: false, + }, + { + desc: "writing exactly switchAfter", + sw: &switchWriter{switchAfter: []byte("UNIQUE")}, + in: []byte("UNIQUE"), + want1: []byte("UNIQUE"), + want2: []byte(""), + wantN: 6, + wantFound: true, + }, + { + desc: "writing before and after switchAfter", + sw: &switchWriter{switchAfter: []byte("UNIQUE")}, + in: []byte("this is before UNIQUE and this is after"), + want1: []byte("this is before UNIQUE"), + want2: []byte(" and this is after"), + wantN: 39, + wantFound: true, + }, + } + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + dst1, dst2 := &bytes.Buffer{}, &bytes.Buffer{} + c.sw.dst1, c.sw.dst2 = dst1, dst2 + n, err := io.Copy(c.sw, iotest.OneByteReader(bytes.NewReader(c.in))) + if err != c.err { + t.Errorf("c.sw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) + } + if n != c.wantN { + t.Errorf("c.sw.Write(%q) = %d, %q, wanted %d, %q", c.in, n, err, c.wantN, c.err) + } + if c.sw.found != c.wantFound { + t.Errorf("c.sw.found = %v, wanted %v", c.sw.found, c.wantFound) + } + if string(dst1.Bytes()) != string(c.want1) { + t.Errorf("dst1.Bytes() = %q, wanted %q", dst1.Bytes(), c.want1) + } + if string(dst2.Bytes()) != string(c.want2) { + t.Errorf("dst2.Bytes() = %q, wanted %q", dst2.Bytes(), c.want2) + } + }) + } +} + +func TestSwitchWriterMultipleWrites(t *testing.T) { + dst1, dst2 := &bytes.Buffer{}, &bytes.Buffer{} + sw := &switchWriter{ + dst1: dst1, + dst2: dst2, + switchAfter: []byte("GOPHER"), + } + n, err := io.Copy(sw, iotest.OneByteReader(strings.NewReader("this is before GO"))) + if err != nil || n != 17 { + t.Errorf("sw.Write(%q) = %d, %q, wanted %d, no error", "this is before GO", n, err, 17) + } + if sw.found { + t.Errorf("sw.found = %v, wanted %v", sw.found, false) + } + if string(dst1.Bytes()) != "this is before GO" { + t.Errorf("dst1.Bytes() = %q, wanted %q", dst1.Bytes(), "this is before GO") + } + if string(dst2.Bytes()) != "" { + t.Errorf("dst2.Bytes() = %q, wanted %q", dst2.Bytes(), "") + } + n, err = io.Copy(sw, iotest.OneByteReader(strings.NewReader("PHER and this is after"))) + if err != nil || n != 22 { + t.Errorf("sw.Write(%q) = %d, %q, wanted %d, no error", "this is before GO", n, err, 22) + } + if !sw.found { + t.Errorf("sw.found = %v, wanted %v", sw.found, true) + } + if string(dst1.Bytes()) != "this is before GOPHER" { + t.Errorf("dst1.Bytes() = %q, wanted %q", dst1.Bytes(), "this is before GOPHEr") + } + if string(dst2.Bytes()) != " and this is after" { + t.Errorf("dst2.Bytes() = %q, wanted %q", dst2.Bytes(), " and this is after") + } +} diff --git a/tests.go b/tests.go index 9e0d55fb..3db7e631 100644 --- a/tests.go +++ b/tests.go @@ -67,7 +67,7 @@ func (s *server) runTests() { continue } if resp.Errors != "" { - stdlog.Print(resp.Errors) + stdlog.Printf("resp.Errors = %q, want %q", resp.Errors, t.errors) failed = true continue } @@ -548,4 +548,26 @@ import "fmt" func Hello() { fmt.Println("hello world") } `, }, + { + name: "timeouts_handled_gracefully", + prog: ` +package main + +import ( + "time" +) + +func main() { + c := make(chan struct{}) + + go func() { + defer close(c) + for { + time.Sleep(10 * time.Millisecond) + } + }() + + <-c +} +`, want: "timeout running program"}, } From b92c87d43880077151dcf7659f28fbdf53f9ce3d Mon Sep 17 00:00:00 2001 From: "Bryan C. Mills" Date: Thu, 16 Apr 2020 13:43:17 -0400 Subject: [PATCH 059/148] playground: make isTest match its documentation This fixes a bug for names such as "Test1", noticed while I was writing a review comment for CL 226757. Change-Id: I1425e380cb2abbb746b108fd97cd8da8f5d40998 Reviewed-on: https://go-review.googlesource.com/c/playground/+/228791 Run-TryBot: Bryan C. Mills TryBot-Result: Gobot Gobot Reviewed-by: Alexander Rakoczy --- sandbox.go | 7 +- sandbox_test.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 sandbox_test.go diff --git a/sandbox.go b/sandbox.go index 07a0f07e..7d4c54e9 100644 --- a/sandbox.go +++ b/sandbox.go @@ -31,6 +31,8 @@ import ( "sync" "text/template" "time" + "unicode" + "unicode/utf8" "cloud.google.com/go/compute/metadata" "github.com/bradfitz/gomemcache/memcache" @@ -190,7 +192,7 @@ func isTestFunc(fn *ast.FuncDecl) bool { // isTest tells whether name looks like a test (or benchmark, according to prefix). // It is a Test (say) if there is a character after Test that is not a lower-case letter. -// We don't want TesticularCancer. +// We don't want mistaken Testimony or erroneous Benchmarking. func isTest(name, prefix string) bool { if !strings.HasPrefix(name, prefix) { return false @@ -198,7 +200,8 @@ func isTest(name, prefix string) bool { if len(name) == len(prefix) { // "Test" is ok return true } - return ast.IsExported(name[len(prefix):]) + r, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(r) } // getTestProg returns source code that executes all valid tests and examples in src. diff --git a/sandbox_test.go b/sandbox_test.go new file mode 100644 index 00000000..f090a487 --- /dev/null +++ b/sandbox_test.go @@ -0,0 +1,183 @@ +// Copyright 2020 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. + +package main + +import ( + "go/token" + "os" + "os/exec" + "reflect" + "runtime" + "strings" + "testing" +) + +// TestIsTest verifies that the isTest helper function matches +// exactly (and only) the names of functions recognized as tests. +func TestIsTest(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.list=.") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, out) + } + t.Logf("%s:\n%s", strings.Join(cmd.Args, " "), out) + + isTestFunction := map[string]bool{} + lines := strings.Split(string(out), "\n") + for _, line := range lines { + isTestFunction[strings.TrimSpace(line)] = true + } + + for _, tc := range []struct { + prefix string + f interface{} + want bool + }{ + {"Test", Test, true}, + {"Test", TestIsTest, true}, + {"Test", Test1IsATest, true}, + {"Test", TestÑIsATest, true}, + + {"Test", TestisNotATest, false}, + + {"Example", Example, true}, + {"Example", ExampleTest, true}, + {"Example", Example_isAnExample, true}, + {"Example", ExampleTest_isAnExample, true}, + + // Example_noOutput has a valid example function name but lacks an output + // declaration, but the isTest function operates only on the test name + // so it cannot detect that the function is not a test. + + {"Example", Example1IsAnExample, true}, + {"Example", ExampleisNotAnExample, false}, + + {"Benchmark", Benchmark, true}, + {"Benchmark", BenchmarkNop, true}, + {"Benchmark", Benchmark1IsABenchmark, true}, + + {"Benchmark", BenchmarkisNotABenchmark, false}, + } { + name := nameOf(t, tc.f) + t.Run(name, func(t *testing.T) { + if tc.want != isTestFunction[name] { + t.Fatalf(".want (%v) is inconsistent with -test.list", tc.want) + } + if !strings.HasPrefix(name, tc.prefix) { + t.Fatalf("%q is not a prefix of %v", tc.prefix, name) + } + + got := isTest(name, tc.prefix) + if got != tc.want { + t.Errorf(`isTest(%q, %q) = %v; want %v`, name, tc.prefix, got, tc.want) + } + }) + } +} + +// nameOf returns the runtime-reported name of function f. +func nameOf(t *testing.T, f interface{}) string { + t.Helper() + + v := reflect.ValueOf(f) + if v.Kind() != reflect.Func { + t.Fatalf("%v is not a function", f) + } + + rf := runtime.FuncForPC(v.Pointer()) + if rf == nil { + t.Fatalf("%v.Pointer() is not a known function", f) + } + + fullName := rf.Name() + parts := strings.Split(fullName, ".") + + name := parts[len(parts)-1] + if !token.IsIdentifier(name) { + t.Fatalf("%q is not a valid identifier", name) + } + return name +} + +// TestisNotATest is not a test function, despite appearances. +// +// Please ignore any lint or vet warnings for this function. +func TestisNotATest(t *testing.T) { + panic("This is not a valid test function.") +} + +// Test11IsATest is a valid test function. +func Test1IsATest(t *testing.T) { +} + +// Test is a test with a minimal name. +func Test(t *testing.T) { +} + +// TestÑIsATest is a test with an interesting Unicode name. +func TestÑIsATest(t *testing.T) { +} + +func Example() { + // Output: +} + +func ExampleTest() { + // This is an example for the function Test. + // ❤ recursion. + Test(nil) + + // Output: +} + +func Example1IsAnExample() { + // Output: +} + +// ExampleisNotAnExample is not an example function, despite appearances. +// +// Please ignore any lint or vet warnings for this function. +func ExampleisNotAnExample() { + panic("This is not a valid example function.") + + // Output: + // None. (This is not really an example function.) +} + +func Example_isAnExample() { + // Output: +} + +func ExampleTest_isAnExample() { + Test(nil) + + // Output: +} + +func Example_noOutput() { + // No output declared: should be compiled but not run. +} + +func Benchmark(b *testing.B) { + for i := 0; i < b.N; i++ { + } +} + +func BenchmarkNop(b *testing.B) { + for i := 0; i < b.N; i++ { + } +} + +func Benchmark1IsABenchmark(b *testing.B) { + for i := 0; i < b.N; i++ { + } +} + +// BenchmarkisNotABenchmark is not a benchmark function, despite appearances. +// +// Please ignore any lint or vet warnings for this function. +func BenchmarkisNotABenchmark(b *testing.B) { + panic("This is not a valid benchmark function.") +} From ede79dd07fdf3a7054ca8fb5f8613d3e671bfe7f Mon Sep 17 00:00:00 2001 From: smasher164 Date: Sun, 19 Apr 2020 23:31:07 -0400 Subject: [PATCH 060/148] playground: update x/mod and x/tools This change pulls in the latest stdlib index indirectly by running $ go get golang.org/x/tools/imports $ go mod tidy which depends on x/tools/internal/imports. As a consequence, using symbols from 1.14, and formatting the source with "Imports" checked should bring in that package. The following program should be re-formatted to import hash/maphash: package main var _ = maphash.Seed Updates golang/go#38464. Change-Id: If5a1291b4a8c471b6a352032f8f9e4664de40106 Reviewed-on: https://go-review.googlesource.com/c/playground/+/228889 Run-TryBot: Akhil Indurti TryBot-Result: Gobot Gobot Reviewed-by: Alexander Rakoczy --- go.mod | 4 ++-- go.sum | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 6fbe3816..5985e8d3 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,8 @@ require ( cloud.google.com/go v0.38.0 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 - golang.org/x/mod v0.1.1-0.20191119225628-919e395dadcd - golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e + golang.org/x/mod v0.2.0 + golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c google.golang.org/api v0.4.0 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 ) diff --git a/go.sum b/go.sum index 0c2bd239..eaab44e2 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,7 @@ github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= @@ -53,8 +54,8 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.1.1-0.20191119225628-919e395dadcd h1:ijnuXbHkHl3nxLcqUTgY+LlU52Zl625Vo8PMDKx+Dyo= -golang.org/x/mod v0.1.1-0.20191119225628-919e395dadcd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -64,6 +65,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= @@ -74,6 +77,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -92,9 +96,13 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c h1:JzwTM5XxGxiCwZEIZQPG46csyhWQxQlu2uSi3bEza34= +golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= From 0084f7fe9d9e9c62544a50c01f05763220ab78e4 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 15 Apr 2020 15:50:35 -0400 Subject: [PATCH 061/148] playground: show a meaningful error on build timeout Add support for returning a meaningful error message to the user, along with partial build output if present. This change also improves graceful command termination throughout the playground. It introduces internal.WaitOrStop, which will wait for a command to finish, or gracefully stop the command if it has not finished by the time the context is cancelled. This also resolves comments from CL 227351. Updates golang/go#38052 Updates golang/go#38530 Updates golang/go#25224 Change-Id: I3a36ad2c5f3626c9cd5b3c1139543ccde3e17ba1 Reviewed-on: https://go-review.googlesource.com/c/playground/+/228438 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Bryan C. Mills Reviewed-by: Dmitri Shuralyov --- internal/internal.go | 66 +++++++++++++++++++++++++++++ sandbox.go | 72 +++++++++++++++++++------------- sandbox/sandbox.go | 99 ++++++++++++++++++++------------------------ 3 files changed, 153 insertions(+), 84 deletions(-) create mode 100644 internal/internal.go diff --git a/internal/internal.go b/internal/internal.go new file mode 100644 index 00000000..1540e93d --- /dev/null +++ b/internal/internal.go @@ -0,0 +1,66 @@ +package internal + +import ( + "context" + "os" + "os/exec" + "time" +) + +// WaitOrStop waits for the already-started command cmd by calling its Wait method. +// +// If cmd does not return before ctx is done, WaitOrStop sends it the given interrupt signal. +// If killDelay is positive, WaitOrStop waits that additional period for Wait to return before sending os.Kill. +func WaitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error { + if cmd.Process == nil { + panic("WaitOrStop called with a nil cmd.Process — missing Start call?") + } + if interrupt == nil { + panic("WaitOrStop requires a non-nil interrupt signal") + } + + errc := make(chan error) + go func() { + select { + case errc <- nil: + return + case <-ctx.Done(): + } + + err := cmd.Process.Signal(interrupt) + if err == nil { + err = ctx.Err() // Report ctx.Err() as the reason we interrupted. + } else if err.Error() == "os: process already finished" { + errc <- nil + return + } + + if killDelay > 0 { + timer := time.NewTimer(killDelay) + select { + // Report ctx.Err() as the reason we interrupted the process... + case errc <- ctx.Err(): + timer.Stop() + return + // ...but after killDelay has elapsed, fall back to a stronger signal. + case <-timer.C: + } + + // Wait still hasn't returned. + // Kill the process harder to make sure that it exits. + // + // Ignore any error: if cmd.Process has already terminated, we still + // want to send ctx.Err() (or the error from the Interrupt call) + // to properly attribute the signal that may have terminated it. + _ = cmd.Process.Kill() + } + + errc <- err + }() + + waitErr := cmd.Wait() + if interruptErr := <-errc; interruptErr != nil { + return interruptErr + } + return waitErr +} diff --git a/sandbox.go b/sandbox.go index 7d4c54e9..b5d5de93 100644 --- a/sandbox.go +++ b/sandbox.go @@ -36,6 +36,7 @@ import ( "cloud.google.com/go/compute/metadata" "github.com/bradfitz/gomemcache/memcache" + "golang.org/x/playground/internal" "golang.org/x/playground/internal/gcpdial" "golang.org/x/playground/sandbox/sandboxtypes" ) @@ -123,6 +124,11 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } + if strings.Contains(resp.Errors, goBuildTimeoutError) { + // TODO(golang.org/issue/38052) - This should be a http.StatusBadRequest, but the UI requires a 200 to parse the response. + s.writeResponse(w, resp, http.StatusOK) + return + } for _, e := range nonCachingErrors { if strings.Contains(resp.Errors, e) { s.log.Errorf("cmdFunc compilation error: %q", resp.Errors) @@ -147,16 +153,21 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context } } - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(resp); err != nil { - s.log.Errorf("error encoding response: %v", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - if _, err := io.Copy(w, &buf); err != nil { - s.log.Errorf("io.Copy(w, &buf): %v", err) - return - } + s.writeResponse(w, resp, http.StatusOK) + } +} + +func (s *server) writeResponse(w http.ResponseWriter, resp *response, status int) { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(resp); err != nil { + s.log.Errorf("error encoding response: %v", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + w.WriteHeader(status) + if _, err := io.Copy(w, &buf); err != nil { + s.log.Errorf("io.Copy(w, &buf): %v", err) + return } } @@ -450,9 +461,7 @@ func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (*bui br.exePath = filepath.Join(tmpDir, "a.out") goCache := filepath.Join(tmpDir, "gocache") - ctx, cancel := context.WithTimeout(ctx, maxCompileTime) - defer cancel() - cmd := exec.CommandContext(ctx, "/usr/local/go-faketime/bin/go", "build", "-o", br.exePath, "-tags=faketime") + cmd := exec.Command("/usr/local/go-faketime/bin/go", "build", "-o", br.exePath, "-tags=faketime") cmd.Dir = tmpDir cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"} cmd.Env = append(cmd.Env, "GOCACHE="+goCache) @@ -472,25 +481,30 @@ func sandboxBuild(ctx context.Context, tmpDir string, in []byte, vet bool) (*bui } cmd.Args = append(cmd.Args, buildPkgArg) cmd.Env = append(cmd.Env, "GOPATH="+br.goPath) - t0 := time.Now() - if out, err := cmd.CombinedOutput(); err != nil { - if ctx.Err() == context.DeadlineExceeded { - log.Printf("go build timed out after %v", time.Since(t0)) - return &buildResult{errorMessage: goBuildTimeoutError}, nil - } - if _, ok := err.(*exec.ExitError); ok { - // Return compile errors to the user. + out := &bytes.Buffer{} + cmd.Stderr, cmd.Stdout = out, out - // Rewrite compiler errors to strip the tmpDir name. - br.errorMessage = strings.Replace(string(out), tmpDir+"/", "", -1) + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("error starting go build: %v", err) + } + ctx, cancel := context.WithTimeout(ctx, maxCompileTime) + defer cancel() + if err := internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + br.errorMessage = fmt.Sprintln(goBuildTimeoutError) + } else if ee := (*exec.ExitError)(nil); !errors.As(err, &ee) { + log.Printf("error building program: %v", err) + return nil, fmt.Errorf("error building go source: %v", err) + } + // Return compile errors to the user. + // Rewrite compiler errors to strip the tmpDir name. + br.errorMessage = br.errorMessage + strings.Replace(string(out.Bytes()), tmpDir+"/", "", -1) - // "go build", invoked with a file name, puts this odd - // message before any compile errors; strip it. - br.errorMessage = strings.Replace(br.errorMessage, "# command-line-arguments\n", "", 1) + // "go build", invoked with a file name, puts this odd + // message before any compile errors; strip it. + br.errorMessage = strings.Replace(br.errorMessage, "# command-line-arguments\n", "", 1) - return br, nil - } - return nil, fmt.Errorf("error building go source: %v", err) + return br, nil } const maxBinarySize = 100 << 20 // copied from sandbox backend; TODO: unify? if fi, err := os.Stat(br.exePath); err != nil || fi.Size() == 0 || fi.Size() > maxBinarySize { diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 07de2867..f8741b74 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -30,6 +30,7 @@ import ( "syscall" "time" + "golang.org/x/playground/internal" "golang.org/x/playground/sandbox/sandboxtypes" ) @@ -70,34 +71,31 @@ var ( ) type Container struct { - name string + name string + stdin io.WriteCloser stdout *limitedWriter stderr *limitedWriter - cmd *exec.Cmd - waitOnce sync.Once - waitVal error + cmd *exec.Cmd + cancelCmd context.CancelFunc + + waitErr chan error // 1-buffered; receives error from WaitOrStop(..., cmd, ...) } func (c *Container) Close() { setContainerWanted(c.name, false) - if c.cmd.Process != nil { - gracefulStop(c.cmd.Process, 250*time.Millisecond) - if err := c.Wait(); err != nil { - log.Printf("error in c.Wait() for %q: %v", c.name, err) - } + c.cancelCmd() + if err := c.Wait(); err != nil { + log.Printf("error in c.Wait() for %q: %v", c.name, err) } } func (c *Container) Wait() error { - c.waitOnce.Do(c.wait) - return c.waitVal -} - -func (c *Container) wait() { - c.waitVal = c.cmd.Wait() + err := <-c.waitErr + c.waitErr <- err + return err } var httpServer *http.Server @@ -267,34 +265,17 @@ func runInGvisor() { if err := cmd.Start(); err != nil { log.Fatalf("cmd.Start(): %v", err) } - timer := time.AfterFunc(runTimeout-(500*time.Millisecond), func() { - fmt.Fprintln(os.Stderr, "timeout running program") - gracefulStop(cmd.Process, 250*time.Millisecond) - }) - defer timer.Stop() - err = cmd.Wait() + ctx, cancel := context.WithTimeout(context.Background(), runTimeout-500*time.Millisecond) + defer cancel() + if err = internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + fmt.Fprintln(os.Stderr, "timeout running program") + } + } os.Exit(errExitCode(err)) return } -// gracefulStop attempts to send a SIGINT before a SIGKILL. -// -// The process will be sent a SIGINT immediately. If the context has still not been cancelled, -// the process will be sent a SIGKILL after delay has passed since sending the SIGINT. -// -// TODO(golang.org/issue/38343) - Change SIGINT to SIGQUIT once decision is made. -func gracefulStop(p *os.Process, delay time.Duration) { - // TODO(golang.org/issue/38343) - Change to syscall.SIGQUIT once decision is made. - if err := p.Signal(os.Interrupt); err != nil { - log.Printf("cmd.Process.Signal(%v): %v", os.Interrupt, err) - } - time.AfterFunc(delay, func() { - if err := p.Kill(); err != nil { - log.Printf("cmd.Process.Kill(): %v", err) - } - }) -} - func makeWorkers() { for { c, err := startContainer(context.Background()) @@ -372,15 +353,25 @@ func startContainer(ctx context.Context) (c *Container, err error) { if err := cmd.Start(); err != nil { return nil, err } + + ctx, cancel := context.WithCancel(ctx) + c = &Container{ + name: name, + stdin: stdin, + stdout: stdout, + stderr: stderr, + cmd: cmd, + cancelCmd: cancel, + waitErr: make(chan error, 1), + } + go func() { + c.waitErr <- internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond) + }() defer func() { if err != nil { - log.Printf("error starting container %q: %v", name, err) - gracefulStop(cmd.Process, 250*time.Millisecond) - setContainerWanted(name, false) + c.Close() } }() - ctx, cancel := context.WithTimeout(ctx, startTimeout) - defer cancel() startErr := make(chan error, 1) go func() { @@ -395,25 +386,23 @@ func startContainer(ctx context.Context) (c *Container, err error) { } }() + timer := time.NewTimer(startTimeout) + defer timer.Stop() select { - case <-ctx.Done(): - err := fmt.Errorf("timeout starting container %q: %w", name, ctx.Err()) - pw.Close() + case <-timer.C: + err := fmt.Errorf("timeout starting container %q", name) + cancel() <-startErr return nil, err - case err = <-startErr: + + case err := <-startErr: if err != nil { return nil, err } } + log.Printf("started container %q", name) - return &Container{ - name: name, - stdin: stdin, - stdout: stdout, - stderr: stderr, - cmd: cmd, - }, nil + return c, nil } func runHandler(w http.ResponseWriter, r *http.Request) { From 11cd822839398b033fec3d4cc0ef07ab272fa067 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Tue, 21 Apr 2020 19:54:15 -0400 Subject: [PATCH 062/148] playground: stop storing timeouts in cache This changes the Playground to stop storing build and run timeout responses in the cache. These responses could get cached when the Playground was unhealthy, leaving some trivial snippets to be cached incorrectly, confusing users. Adds testing for our caching logic. Updates golang/go#38546 Updates golang/go#38576 Change-Id: Idd2106d673162d9eea8536fe2433f74c23ed6e8a Reviewed-on: https://go-review.googlesource.com/c/playground/+/229307 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov --- cache.go | 8 +++ go.mod | 1 + main.go | 1 + sandbox.go | 30 ++++++---- server.go | 2 +- server_test.go | 149 +++++++++++++++++++++++++++++++++++++------------ 6 files changed, 143 insertions(+), 48 deletions(-) diff --git a/cache.go b/cache.go index 5d725ba7..dca1d23f 100644 --- a/cache.go +++ b/cache.go @@ -11,6 +11,14 @@ import ( "github.com/bradfitz/gomemcache/memcache" ) +// responseCache is a common interface for cache implementations. +type responseCache interface { + // Set sets the value for a key. + Set(key string, v interface{}) error + // Get sets v to the value stored for a key. + Get(key string, v interface{}) error +} + // gobCache stores and retrieves values using a memcache client using the gob // encoding package. It does not currently allow for expiration of items. // With a nil gobCache, Set is a no-op and Get will always return memcache.ErrCacheMiss. diff --git a/go.mod b/go.mod index 5985e8d3..b5c0073d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( cloud.google.com/go v0.38.0 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 + github.com/google/go-cmp v0.3.0 golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 golang.org/x/mod v0.2.0 golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c diff --git a/main.go b/main.go index 733a6a04..af9774ed 100644 --- a/main.go +++ b/main.go @@ -39,6 +39,7 @@ func main() { s.cache = newGobCache(caddr) log.Printf("App (project ID: %q) is caching results", pid) } else { + s.cache = (*gobCache)(nil) // Use a no-op cache implementation. log.Printf("App (project ID: %q) is NOT caching results", pid) } s.log = log diff --git a/sandbox.go b/sandbox.go index b5d5de93..76fd7fa2 100644 --- a/sandbox.go +++ b/sandbox.go @@ -50,14 +50,16 @@ const ( progName = "prog.go" ) -const goBuildTimeoutError = "timeout running go build" +const ( + goBuildTimeoutError = "timeout running go build" + runTimeoutError = "timeout running program" +) -// Responses that contain these strings will not be cached due to -// their non-deterministic nature. -var nonCachingErrors = []string{ +// internalErrors are strings found in responses that will not be cached +// due to their non-deterministic nature. +var internalErrors = []string{ "out of memory", "cannot allocate memory", - goBuildTimeoutError, } type request struct { @@ -115,7 +117,7 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context resp := &response{} key := cacheKey(cachePrefix, req.Body) if err := s.cache.Get(key, resp); err != nil { - if err != memcache.ErrCacheMiss { + if !errors.Is(err, memcache.ErrCacheMiss) { s.log.Errorf("s.cache.Get(%q, &response): %v", key, err) } resp, err = cmdFunc(r.Context(), &req) @@ -124,12 +126,16 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - if strings.Contains(resp.Errors, goBuildTimeoutError) { - // TODO(golang.org/issue/38052) - This should be a http.StatusBadRequest, but the UI requires a 200 to parse the response. + if strings.Contains(resp.Errors, goBuildTimeoutError) || strings.Contains(resp.Errors, runTimeoutError) { + // TODO(golang.org/issue/38576) - This should be a http.StatusBadRequest, + // but the UI requires a 200 to parse the response. It's difficult to know + // if we've timed out because of an error in the code snippet, or instability + // on the playground itself. Either way, we should try to show the user the + // partial output of their program. s.writeResponse(w, resp, http.StatusOK) return } - for _, e := range nonCachingErrors { + for _, e := range internalErrors { if strings.Contains(resp.Errors, e) { s.log.Errorf("cmdFunc compilation error: %q", resp.Errors) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -140,7 +146,7 @@ func (s *server) commandHandler(cachePrefix string, cmdFunc func(context.Context if el.Kind != "stderr" { continue } - for _, e := range nonCachingErrors { + for _, e := range internalErrors { if strings.Contains(el.Message, e) { s.log.Errorf("cmdFunc runtime error: %q", el.Message) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -543,8 +549,8 @@ func sandboxRun(ctx context.Context, exePath string, testParam string) (sandboxt sreq.GetBody = func() (io.ReadCloser, error) { return ioutil.NopCloser(bytes.NewReader(exeBytes)), nil } res, err := sandboxBackendClient().Do(sreq) if err != nil { - if ctx.Err() == context.DeadlineExceeded { - execRes.Error = "timeout running program" + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + execRes.Error = runTimeoutError return execRes, nil } return execRes, fmt.Errorf("POST %q: %w", sandboxBackendURL(), err) diff --git a/server.go b/server.go index f091933d..91616338 100644 --- a/server.go +++ b/server.go @@ -18,7 +18,7 @@ type server struct { mux *http.ServeMux db store log logger - cache *gobCache + cache responseCache // When the executable was last modified. Used for caching headers of compiled assets. modtime time.Time diff --git a/server_test.go b/server_test.go index a7d9fac2..7b6f77b7 100644 --- a/server_test.go +++ b/server_test.go @@ -7,12 +7,17 @@ package main import ( "bytes" "context" + "encoding/json" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" + "sync" "testing" + + "github.com/bradfitz/gomemcache/memcache" + "github.com/google/go-cmp/cmp" ) type testLogger struct { @@ -166,6 +171,7 @@ func TestCommandHandler(t *testing.T) { // Should we verify that s.log.Errorf was called // instead of just printing or failing the test? s.log = newStdLogger() + s.cache = new(inMemCache) return nil }) if err != nil { @@ -193,63 +199,103 @@ func TestCommandHandler(t *testing.T) { if r.Body == "allocate-memory-compile-error" { return &response{Errors: "cannot allocate memory"}, nil } + if r.Body == "build-timeout-error" { + return &response{Errors: goBuildTimeoutError}, nil + } + if r.Body == "run-timeout-error" { + return &response{Errors: runTimeoutError}, nil + } resp := &response{Events: []Event{{r.Body, "stdout", 0}}} return resp, nil }) testCases := []struct { - desc string - method string - statusCode int - reqBody []byte - respBody []byte + desc string + method string + statusCode int + reqBody []byte + respBody []byte + shouldCache bool }{ - {"OPTIONS request", http.MethodOptions, http.StatusOK, nil, nil}, - {"GET request", http.MethodGet, http.StatusBadRequest, nil, nil}, - {"Empty POST", http.MethodPost, http.StatusBadRequest, nil, nil}, - {"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil}, + {"OPTIONS request", http.MethodOptions, http.StatusOK, nil, nil, false}, + {"GET request", http.MethodGet, http.StatusBadRequest, nil, nil, false}, + {"Empty POST", http.MethodPost, http.StatusBadRequest, nil, nil, false}, + {"Failed cmdFunc", http.MethodPost, http.StatusInternalServerError, []byte(`{"Body":"fail"}`), nil, false}, {"Standard flow", http.MethodPost, http.StatusOK, []byte(`{"Body":"ok"}`), []byte(`{"Errors":"","Events":[{"Message":"ok","Kind":"stdout","Delay":0}],"Status":0,"IsTest":false,"TestsFailed":0} `), - }, - {"Errors in response", http.MethodPost, http.StatusOK, + true}, + {"Cache-able Errors in response", http.MethodPost, http.StatusOK, []byte(`{"Body":"error"}`), []byte(`{"Errors":"errors","Events":null,"Status":0,"IsTest":false,"TestsFailed":0} `), - }, + true}, {"Out of memory error in response body event message", http.MethodPost, http.StatusInternalServerError, - []byte(`{"Body":"oom-error"}`), nil}, + []byte(`{"Body":"oom-error"}`), nil, false}, {"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError, - []byte(`{"Body":"allocate-memory-error"}`), nil}, + []byte(`{"Body":"allocate-memory-error"}`), nil, false}, {"Out of memory error in response errors", http.MethodPost, http.StatusInternalServerError, - []byte(`{"Body":"oom-compile-error"}`), nil}, + []byte(`{"Body":"oom-compile-error"}`), nil, false}, {"Cannot allocate memory error in response errors", http.MethodPost, http.StatusInternalServerError, - []byte(`{"Body":"allocate-memory-compile-error"}`), nil}, + []byte(`{"Body":"allocate-memory-compile-error"}`), nil, false}, + { + desc: "Build timeout error", + method: http.MethodPost, + statusCode: http.StatusOK, + reqBody: []byte(`{"Body":"build-timeout-error"}`), + respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running go build","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), + }, + { + desc: "Run timeout error", + method: http.MethodPost, + statusCode: http.StatusOK, + reqBody: []byte(`{"Body":"run-timeout-error"}`), + respBody: []byte(fmt.Sprintln(`{"Errors":"timeout running program","Events":null,"Status":0,"IsTest":false,"TestsFailed":0}`)), + }, } for _, tc := range testCases { - req := httptest.NewRequest(tc.method, "/compile", bytes.NewReader(tc.reqBody)) - w := httptest.NewRecorder() - testHandler(w, req) - resp := w.Result() - corsHeader := "Access-Control-Allow-Origin" - if got, want := resp.Header.Get(corsHeader), "*"; got != want { - t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) - } - if got, want := resp.StatusCode, tc.statusCode; got != want { - t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) - } - if tc.respBody != nil { - defer resp.Body.Close() - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err) + t.Run(tc.desc, func(t *testing.T) { + req := httptest.NewRequest(tc.method, "/compile", bytes.NewReader(tc.reqBody)) + w := httptest.NewRecorder() + testHandler(w, req) + resp := w.Result() + corsHeader := "Access-Control-Allow-Origin" + if got, want := resp.Header.Get(corsHeader), "*"; got != want { + t.Errorf("%s: %q header: got %q; want %q", tc.desc, corsHeader, got, want) } - if !bytes.Equal(b, tc.respBody) { - t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody) + if got, want := resp.StatusCode, tc.statusCode; got != want { + t.Errorf("%s: got unexpected status code %d; want %d", tc.desc, got, want) } - } + if tc.respBody != nil { + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err) + } + if !bytes.Equal(b, tc.respBody) { + t.Errorf("%s: got unexpected body %q; want %q", tc.desc, b, tc.respBody) + } + } + + // Test caching semantics. + sbreq := new(request) // A sandbox request, used in the cache key. + json.Unmarshal(tc.reqBody, sbreq) // Ignore errors, request may be empty. + gotCache := new(response) + if err := s.cache.Get(cacheKey("test", sbreq.Body), gotCache); (err == nil) != tc.shouldCache { + t.Errorf("s.cache.Get(%q, %v) = %v, shouldCache: %v", cacheKey("test", sbreq.Body), gotCache, err, tc.shouldCache) + } + wantCache := new(response) + if tc.shouldCache { + if err := json.Unmarshal(tc.respBody, wantCache); err != nil { + t.Errorf("json.Unmarshal(%q, %v) = %v, wanted no error", tc.respBody, wantCache, err) + } + } + if diff := cmp.Diff(wantCache, gotCache); diff != "" { + t.Errorf("s.Cache.Get(%q) mismatch (-want +got):\n%s", cacheKey("test", sbreq.Body), diff) + } + }) } } @@ -315,3 +361,36 @@ func TestPlaygroundGoproxy(t *testing.T) { }) } } + +// inMemCache is a responseCache backed by a map. It is only suitable for testing. +type inMemCache struct { + l sync.Mutex + m map[string]*response +} + +// Set implements the responseCache interface. +// Set stores a *response in the cache. It panics for other types to ensure test failure. +func (i *inMemCache) Set(key string, v interface{}) error { + i.l.Lock() + defer i.l.Unlock() + if i.m == nil { + i.m = make(map[string]*response) + } + i.m[key] = v.(*response) + return nil +} + +// Get implements the responseCache interface. +// Get fetches a *response from the cache, or returns a memcache.ErrcacheMiss. +// It panics for other types to ensure test failure. +func (i *inMemCache) Get(key string, v interface{}) error { + i.l.Lock() + defer i.l.Unlock() + target := v.(*response) + got, ok := i.m[key] + if !ok { + return memcache.ErrCacheMiss + } + *target = *got + return nil +} From b8c0e2b9578f3ccf6aa5a57f7b5b17d5c2b16751 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Thu, 23 Apr 2020 11:35:30 -0400 Subject: [PATCH 063/148] playground: only build binaries for health check This change avoids cascading sandbox backend failures to the frontend instances. The timeout TODO is safe to remove as we now pass in the request context for cancellation, and sandboxBuild has its own build timeout. Updates golang/go#25224 Updates golang/go#38530 Change-Id: If892f86bad08c55429b6ebab768b83c5d7621cf1 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229677 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov Reviewed-by: Carlos Amedee --- sandbox.go | 19 +++++++++++-------- server.go | 2 +- tests.go | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/sandbox.go b/sandbox.go index 76fd7fa2..8931100c 100644 --- a/sandbox.go +++ b/sandbox.go @@ -593,17 +593,20 @@ func playgroundGoproxy() string { return "https://proxy.golang.org" } -func (s *server) healthCheck() error { - ctx := context.Background() // TODO: cap it to some reasonable timeout - resp, err := compileAndRun(ctx, &request{Body: healthProg}) +// healthCheck attempts to build a binary from the source in healthProg. +// It returns any error returned from sandboxBuild, or nil if none is returned. +func (s *server) healthCheck(ctx context.Context) error { + tmpDir, err := ioutil.TempDir("", "sandbox") if err != nil { - return err + return fmt.Errorf("error creating temp directory: %v", err) } - if resp.Errors != "" { - return fmt.Errorf("compile error: %v", resp.Errors) + defer os.RemoveAll(tmpDir) + br, err := sandboxBuild(ctx, tmpDir, []byte(healthProg), false) + if err != nil { + return err } - if len(resp.Events) != 1 || resp.Events[0].Message != "ok" { - return fmt.Errorf("unexpected output: %v", resp.Events) + if br.errorMessage != "" { + return errors.New(br.errorMessage) } return nil } diff --git a/server.go b/server.go index 91616338..8352c511 100644 --- a/server.go +++ b/server.go @@ -75,7 +75,7 @@ func handleFavicon(w http.ResponseWriter, r *http.Request) { } func (s *server) handleHealthCheck(w http.ResponseWriter, r *http.Request) { - if err := s.healthCheck(); err != nil { + if err := s.healthCheck(r.Context()); err != nil { http.Error(w, "Health check failed: "+err.Error(), http.StatusInternalServerError) return } diff --git a/tests.go b/tests.go index 3db7e631..840a5e77 100644 --- a/tests.go +++ b/tests.go @@ -37,7 +37,7 @@ func (s *server) test() { } func (s *server) runTests() { - if err := s.healthCheck(); err != nil { + if err := s.healthCheck(context.Background()); err != nil { stdlog.Fatal(err) } From 8da5bd692b8899b0fef32cc02be2c70901661208 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 22 Apr 2020 13:00:14 -0400 Subject: [PATCH 064/148] sandbox: update configuration to match production - Specify the correct image in konlet.yaml. gvisor-playground-sandbox is the child-process container. - Correct interpolation in config identifiers, which is deprecated. - Set min_ready_sec for update policy to not cause an outage when updating - Use name_prefix for instance_template instead of name, which allows updates. Templates are immutable, so previously this was not possible to update. Updates golang/go#38530 Updates golang/go#25224 Change-Id: I3f7618b8e378eaa9714e571b90390b7052bf2855 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229418 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov --- sandbox/konlet.yaml | 2 +- sandbox/sandbox.tf | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/sandbox/konlet.yaml b/sandbox/konlet.yaml index 2850af34..1f424569 100644 --- a/sandbox/konlet.yaml +++ b/sandbox/konlet.yaml @@ -1,7 +1,7 @@ spec: containers: - name: playground - image: 'gcr.io/PROJECT_NAME/gvisor-playground-sandbox:latest' + image: 'gcr.io/PROJECT_NAME/playground-sandbox:latest' volumeMounts: - name: dockersock mountPath: /var/run/docker.sock diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf index 14de7cbc..d8ca4637 100644 --- a/sandbox/sandbox.tf +++ b/sandbox/sandbox.tf @@ -35,9 +35,9 @@ data "google_compute_image" "cos" { } resource "google_compute_instance_template" "inst_tmpl" { - name = "play-sandbox-tmpl" + name_prefix = "play-sandbox-tmpl" machine_type = "n1-standard-8" - metadata = { + metadata = { "ssh-keys" = "bradfitz:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDaRpEbckQ+harGnrKUjk3JziwYqvz2bRNn0ngpzROaeCwm1XetDby/fgmQruZE/OBpbeOaCOd/yyP89Oer9CJx41AFEfHbudePZti/y+fmZ05N+QoBSAG0JtYWVydIjAjCenKBbNrYmwcQ840uNdIv9Ztqu3lbO/syMgcajappzdqMlwVZuHTJUe1JQD355PiinFHPTa7l0MrZPfiSsBdiTGmO39iVa312yshu6dZAvDgRL+bgIzTL6udPL/cVq+zlkvoZbzC4ajuZs4w2in+kqXHQSxbKHlXOhPrej1fwhspm+0Y7hEZOaN5Juc5GseNCHImtJh1rei1Qa4U/nTjt bradfitz@bradfitz-dev" "gce-container-declaration" = data.local_file.konlet.content "user-data" = data.local_file.cloud_init.content @@ -63,11 +63,11 @@ resource "google_compute_instance_template" "inst_tmpl" { } resource "google_compute_region_autoscaler" "default" { - provider = "google-beta" + provider = google-beta name = "play-sandbox-autoscaler" region = "us-central1" - target = "${google_compute_region_instance_group_manager.rigm.self_link}" + target = google_compute_region_instance_group_manager.rigm.self_link autoscaling_policy { max_replicas = 10 @@ -81,7 +81,7 @@ resource "google_compute_region_autoscaler" "default" { } resource "google_compute_region_instance_group_manager" "rigm" { - provider = "google-beta" + provider = google-beta name = "play-sandbox-rigm" base_instance_name = "playsandbox" @@ -89,16 +89,24 @@ resource "google_compute_region_instance_group_manager" "rigm" { version { name = "primary" - instance_template = "${google_compute_instance_template.inst_tmpl.self_link}" + instance_template = google_compute_instance_template.inst_tmpl.self_link } named_port { name = "http" port = 80 } + update_policy { + type = "PROACTIVE" + instance_redistribution_type = "PROACTIVE" + minimal_action = "REPLACE" + max_surge_fixed = 10 + max_unavailable_fixed = 0 + min_ready_sec = 60 + } } data "google_compute_region_instance_group" "rig" { - provider = "google-beta" - self_link = "${google_compute_region_instance_group_manager.rigm.instance_group}" + provider = google-beta + self_link = google_compute_region_instance_group_manager.rigm.instance_group } From a90f64aaa4e66319e7e970d934242a90f10804c8 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Fri, 24 Apr 2020 12:11:39 -0400 Subject: [PATCH 065/148] sandbox: change from n1 to e2 instances This conforms with a policy change, and should save money with no measurable performance impact. Removes bradfitz's SSH key from instances (that are inaccessible anyway). Updates golang/go#25224 Change-Id: I1733192c98deee1deabf2237ae5fe19edd29ab93 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229957 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Carlos Amedee --- sandbox/sandbox.tf | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf index d8ca4637..49bf8d58 100644 --- a/sandbox/sandbox.tf +++ b/sandbox/sandbox.tf @@ -36,9 +36,8 @@ data "google_compute_image" "cos" { resource "google_compute_instance_template" "inst_tmpl" { name_prefix = "play-sandbox-tmpl" - machine_type = "n1-standard-8" + machine_type = "e2-standard-8" metadata = { - "ssh-keys" = "bradfitz:ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDaRpEbckQ+harGnrKUjk3JziwYqvz2bRNn0ngpzROaeCwm1XetDby/fgmQruZE/OBpbeOaCOd/yyP89Oer9CJx41AFEfHbudePZti/y+fmZ05N+QoBSAG0JtYWVydIjAjCenKBbNrYmwcQ840uNdIv9Ztqu3lbO/syMgcajappzdqMlwVZuHTJUe1JQD355PiinFHPTa7l0MrZPfiSsBdiTGmO39iVa312yshu6dZAvDgRL+bgIzTL6udPL/cVq+zlkvoZbzC4ajuZs4w2in+kqXHQSxbKHlXOhPrej1fwhspm+0Y7hEZOaN5Juc5GseNCHImtJh1rei1Qa4U/nTjt bradfitz@bradfitz-dev" "gce-container-declaration" = data.local_file.konlet.content "user-data" = data.local_file.cloud_init.content } From 91bfd78339d9d36dc610e90a69fe1659f2504fd7 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Thu, 23 Apr 2020 18:06:30 -0400 Subject: [PATCH 066/148] sandbox: add stackdriver scope This changes the sandbox VMs to allow them to write to StackDriver metrics. Updates golang/go#25224 Updates golang/go#38530 Change-Id: I82954b8eed3664289f5c69c0f5301a72206f0948 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229681 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov --- sandbox/sandbox.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sandbox/sandbox.tf b/sandbox/sandbox.tf index 49bf8d58..4b8f8396 100644 --- a/sandbox/sandbox.tf +++ b/sandbox/sandbox.tf @@ -45,7 +45,7 @@ resource "google_compute_instance_template" "inst_tmpl" { network = "golang" } service_account { - scopes = ["logging-write", "storage-ro"] + scopes = ["logging-write", "storage-ro", "monitoring-write"] } disk { source_image = data.google_compute_image.cos.self_link From 7bdfbfb0d0cffd2504562896fd4b31d580eeeb65 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 22 Apr 2020 20:18:34 -0400 Subject: [PATCH 067/148] sandbox: instrument HTTP handlers with StackDriver This change adds OpenCensus's HTTP instrumentation to the sandbox. In development mode, it exposes a prometheus metrics interface on /statusz. This is the first in a series of CLs to add instrumentation to different parts of the sandbox to help investigate instability issues. For now, reporting metrics around our responses per handler to StackDriver will be helpful. OpenTelemetry would be preferable, as it is the successor of OpenCensus, however the StackDriver integration is not quite done. Updates golang/go#25224 Updates golang/go#38530 Change-Id: I600fd695bb66c8bee16bc0b778d51930f4cdd476 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229679 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov Reviewed-by: Carlos Amedee --- go.mod | 14 ++- go.sum | 269 ++++++++++++++++++++++++++++++++++++++++ sandbox/Dockerfile | 5 + sandbox/metrics.go | 164 ++++++++++++++++++++++++ sandbox/sandbox.go | 25 +++- sandbox/sandbox_test.go | 4 + 6 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 sandbox/metrics.go diff --git a/go.mod b/go.mod index b5c0073d..89733836 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,20 @@ module golang.org/x/playground go 1.12 require ( - cloud.google.com/go v0.38.0 + cloud.google.com/go v0.54.0 + cloud.google.com/go/datastore v1.1.0 + cloud.google.com/go/pubsub v1.3.1 // indirect + contrib.go.opencensus.io/exporter/prometheus v0.1.0 + contrib.go.opencensus.io/exporter/stackdriver v0.13.1 github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 - github.com/google/go-cmp v0.3.0 + github.com/google/go-cmp v0.4.0 + go.opencensus.io v0.22.3 golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 golang.org/x/mod v0.2.0 golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c - google.golang.org/api v0.4.0 + google.golang.org/api v0.20.0 + google.golang.org/genproto v0.0.0-20200312145019-da6875a35672 + gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v2 v2.2.7 // indirect grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 ) diff --git a/go.sum b/go.sum index eaab44e2..da9e9cbe 100644 --- a/go.sum +++ b/go.sum @@ -2,100 +2,307 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1 h1:lRi0CHyU+ytlvylOlFKKq0af6JncuyoRh1J+QJBqQx0= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0 h1:K2NyuHRuv15ku6eUpe0DQk5ZykPMnSOnvuVf6IHcjaE= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1 h1:ukjixP1wl0LpnZ6LWtZJ0mX5tBmjp1f8Sqer8Z2OMUU= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0 h1:UDpwYIwla4jHGzZJaEJYx1tOejbgSoNqsAfHAUYe2r8= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +contrib.go.opencensus.io/exporter/prometheus v0.1.0 h1:SByaIoWwNgMdPSgl5sMqM2KDE5H/ukPWBRo314xiDvg= +contrib.go.opencensus.io/exporter/prometheus v0.1.0/go.mod h1:cGFniUXGZlKRjzOyuZJ6mgB+PgBcCIa79kEKR8YCW+A= +contrib.go.opencensus.io/exporter/stackdriver v0.13.1 h1:RX9W6FelAqTVnBi/bRXJLXr9n18v4QkQwZYIdnNS51I= +contrib.go.opencensus.io/exporter/stackdriver v0.13.1/go.mod h1:z2tyTZtPmQ2HvWH4cOmVDgtY+1lomfKdbLnkJvZdc8c= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/aws/aws-sdk-go v1.23.20 h1:2CBuL21P0yKdZN5urf2NxKa1ha8fhnY+A3pBCHFeZoA= +github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668 h1:U/lr3Dgy4WK+hNk4tyD+nuGjpVLPEHuJSFXMw11/HPA= github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= +github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8= +github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE= +github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.1 h1:8dP3SGL7MPB94crU3bEPplMPe83FI4EouesJUeFHv50= +go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0 h1:TJegdELkXYRfV3+4F6OEL777GyidAUcamDxkvQhIZCA= golang.org/x/build v0.0.0-20190709001953-30c0e6b89ea0/go.mod h1:fYw7AShPAhGMdXqA9gRadk/CcMsvLlClpE5oBwnS3dM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190424203555-c05e17bb3b2d/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 h1:OeRHuibLsmZkFj773W4LcfAGsSxJgfPONhr8cmO+eLA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82 h1:vsphBvatvfbhlb4PO1BYSr9dzugGxJ/SQHoNufZJq1w= golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e h1:aZzprAO9/8oim3qStq3wc1Xuxx4QmAGriC4VU4ojemQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c h1:JzwTM5XxGxiCwZEIZQPG46csyhWQxQlu2uSi3bEza34= golang.org/x/tools v0.0.0-20200420001825-978e26b7c37c/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -105,20 +312,82 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.10.0 h1:7tmAxx3oKE98VMZ+SBZzvYYWRQ9HODBxmC8mXUsraSQ= +google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.2 h1:j8RI1yW0SkI+paT6uGwMlrMI/6zwYA6/CFil8rxOzGI= +google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873 h1:nfPFGzJkUDX6uBmpN/pSw7MbOAWegH5QDQuoXFHedLg= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672 h1:jiDSspVssiikoRPFHT6pYrL+CL6/yIc3b9AuHO/4xik= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919 h1:tmXTu+dfa+d9Evp8NpJdgOy6+rt8/x4yG7qPBrtNfLY= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/sandbox/Dockerfile b/sandbox/Dockerfile index f2da1d59..bd32b1e8 100644 --- a/sandbox/Dockerfile +++ b/sandbox/Dockerfile @@ -6,6 +6,11 @@ FROM golang:1.14 AS build +COPY go.mod /go/src/playground/go.mod +COPY go.sum /go/src/playground/go.sum +WORKDIR /go/src/playground +RUN go mod download + COPY . /go/src/playground WORKDIR /go/src/playground/sandbox RUN go install diff --git a/sandbox/metrics.go b/sandbox/metrics.go new file mode 100644 index 00000000..9ec454de --- /dev/null +++ b/sandbox/metrics.go @@ -0,0 +1,164 @@ +// Copyright 2020 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. + +package main + +import ( + "fmt" + "net/http" + "time" + + "cloud.google.com/go/compute/metadata" + "contrib.go.opencensus.io/exporter/prometheus" + "contrib.go.opencensus.io/exporter/stackdriver" + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/stats/view" + "go.opencensus.io/tag" + mrpb "google.golang.org/genproto/googleapis/api/monitoredres" +) + +// Customizations of ochttp views. Views are updated as follows: +// * The views are prefixed with go-playground-sandbox. +// * ochttp.KeyServerRoute is added as a tag to label metrics per-route. +var ( + ServerRequestCountView = &view.View{ + Name: "go-playground-sandbox/http/server/request_count", + Description: "Count of HTTP requests started", + Measure: ochttp.ServerRequestCount, + TagKeys: []tag.Key{ochttp.KeyServerRoute}, + Aggregation: view.Count(), + } + ServerRequestBytesView = &view.View{ + Name: "go-playground-sandbox/http/server/request_bytes", + Description: "Size distribution of HTTP request body", + Measure: ochttp.ServerRequestBytes, + TagKeys: []tag.Key{ochttp.KeyServerRoute}, + Aggregation: ochttp.DefaultSizeDistribution, + } + ServerResponseBytesView = &view.View{ + Name: "go-playground-sandbox/http/server/response_bytes", + Description: "Size distribution of HTTP response body", + Measure: ochttp.ServerResponseBytes, + TagKeys: []tag.Key{ochttp.KeyServerRoute}, + Aggregation: ochttp.DefaultSizeDistribution, + } + ServerLatencyView = &view.View{ + Name: "go-playground-sandbox/http/server/latency", + Description: "Latency distribution of HTTP requests", + Measure: ochttp.ServerLatency, + TagKeys: []tag.Key{ochttp.KeyServerRoute}, + Aggregation: ochttp.DefaultLatencyDistribution, + } + ServerRequestCountByMethod = &view.View{ + Name: "go-playground-sandbox/http/server/request_count_by_method", + Description: "Server request count by HTTP method", + TagKeys: []tag.Key{ochttp.Method, ochttp.KeyServerRoute}, + Measure: ochttp.ServerRequestCount, + Aggregation: view.Count(), + } + ServerResponseCountByStatusCode = &view.View{ + Name: "go-playground-sandbox/http/server/response_count_by_status_code", + Description: "Server response count by status code", + TagKeys: []tag.Key{ochttp.StatusCode, ochttp.KeyServerRoute}, + Measure: ochttp.ServerLatency, + Aggregation: view.Count(), + } +) + +// newMetricService initializes a *metricService. +// +// The metricService returned is configured to send metric data to StackDriver. +// When the sandbox is not running on GCE, it will host metrics through a prometheus HTTP handler. +func newMetricService() (*metricService, error) { + err := view.Register( + ServerRequestCountView, + ServerRequestBytesView, + ServerResponseBytesView, + ServerLatencyView, + ServerRequestCountByMethod, + ServerResponseCountByStatusCode) + if err != nil { + return nil, err + } + + if !metadata.OnGCE() { + view.SetReportingPeriod(5 * time.Second) + pe, err := prometheus.NewExporter(prometheus.Options{}) + if err != nil { + return nil, fmt.Errorf("newMetricsService(): prometheus.NewExporter: %w", err) + } + view.RegisterExporter(pe) + return &metricService{pExporter: pe}, nil + } + + projID, err := metadata.ProjectID() + if err != nil { + return nil, err + } + zone, err := metadata.Zone() + if err != nil { + return nil, err + } + iname, err := metadata.InstanceName() + if err != nil { + return nil, err + } + + sd, err := stackdriver.NewExporter(stackdriver.Options{ + ProjectID: projID, + MonitoredResource: (*monitoredResource)(&mrpb.MonitoredResource{ + Type: "generic_task", + Labels: map[string]string{ + "instance_id": iname, + "job": "go-playground-sandbox", + "project_id": projID, + "zone": zone, + }, + }), + ReportingInterval: time.Minute, // Minimum interval for stackdriver is 1 minute. + }) + if err != nil { + return nil, err + } + + // Minimum interval for stackdriver is 1 minute. + view.SetReportingPeriod(time.Minute) + view.RegisterExporter(sd) + // Start the metrics exporter. + if err := sd.StartMetricsExporter(); err != nil { + return nil, err + } + + return &metricService{sdExporter: sd}, nil +} + +type metricService struct { + sdExporter *stackdriver.Exporter + pExporter *prometheus.Exporter +} + +func (m *metricService) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if m.pExporter != nil { + m.pExporter.ServeHTTP(w, r) + return + } + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) +} + +func (m *metricService) Stop() { + if sde := m.sdExporter; sde != nil { + // Flush any unsent data before exiting. + sde.Flush() + + sde.StopMetricsExporter() + } +} + +// monitoredResource wraps a *mrpb.MonitoredResource to implement the +// monitoredresource.MonitoredResource interface. +type monitoredResource mrpb.MonitoredResource + +func (r *monitoredResource) MonitoredResource() (resType string, labels map[string]string) { + return r.Type, r.Labels +} diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index f8741b74..f2a49e78 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -30,6 +30,8 @@ import ( "syscall" "time" + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/trace" "golang.org/x/playground/internal" "golang.org/x/playground/sandbox/sandboxtypes" ) @@ -116,6 +118,15 @@ func main() { runSem = make(chan struct{}, *numWorkers) go handleSignals() + mux := http.NewServeMux() + + if ms, err := newMetricService(); err != nil { + log.Printf("Failed to initialize metrics: newMetricService() = _, %v, wanted no error", err) + } else { + mux.Handle("/statusz", ochttp.WithRouteTag(ms, "/statusz")) + defer ms.Stop() + } + if out, err := exec.Command("docker", "version").CombinedOutput(); err != nil { log.Fatalf("failed to connect to docker: %v, %s", err, out) } @@ -129,14 +140,18 @@ func main() { log.Printf("Listening on %s", *listenAddr) } - http.HandleFunc("/health", healthHandler) - http.HandleFunc("/healthz", healthHandler) - http.HandleFunc("/", rootHandler) - http.HandleFunc("/run", runHandler) + mux.Handle("/health", ochttp.WithRouteTag(http.HandlerFunc(healthHandler), "/health")) + mux.Handle("/healthz", ochttp.WithRouteTag(http.HandlerFunc(healthHandler), "/healthz")) + mux.Handle("/", ochttp.WithRouteTag(http.HandlerFunc(rootHandler), "/")) + mux.Handle("/run", ochttp.WithRouteTag(http.HandlerFunc(runHandler), "/run")) go makeWorkers() - httpServer = &http.Server{Addr: *listenAddr} + trace.ApplyConfig(trace.Config{DefaultSampler: trace.NeverSample()}) + httpServer = &http.Server{ + Addr: *listenAddr, + Handler: &ochttp.Handler{Handler: mux}, + } log.Fatal(httpServer.ListenAndServe()) } diff --git a/sandbox/sandbox_test.go b/sandbox/sandbox_test.go index 7b32ec6e..c5ea09a0 100644 --- a/sandbox/sandbox_test.go +++ b/sandbox/sandbox_test.go @@ -1,3 +1,7 @@ +// Copyright 2020 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. + package main import ( From ccdf1e5d22bc9653f3a3b58016d2c9914ebaad48 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Wed, 22 Apr 2020 20:18:34 -0400 Subject: [PATCH 068/148] sandbox: instrument number of running containers This change adds a metric to count the number of running containers, according to Docker. Updates golang/go#25224 Updates golang/go#38530 Change-Id: Id989986928dff594cb1de0903a56dcffed8220c4 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229680 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Carlos Amedee --- internal/internal.go | 18 +++++++++ internal/internal_test.go | 48 ++++++++++++++++++++++++ sandbox/metrics.go | 30 +++++++++++++++ sandbox/sandbox.go | 79 ++++++++++++++++++++++++++++++++++++++- sandbox/sandbox_test.go | 42 +++++++++++++++++++++ 5 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 internal/internal_test.go diff --git a/internal/internal.go b/internal/internal.go index 1540e93d..274bf2c9 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -1,3 +1,7 @@ +// Copyright 2020 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. + package internal import ( @@ -64,3 +68,17 @@ func WaitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDel } return waitErr } + +// PeriodicallyDo calls f every period until the provided context is cancelled. +func PeriodicallyDo(ctx context.Context, period time.Duration, f func(context.Context, time.Time)) { + ticker := time.NewTicker(period) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case now := <-ticker.C: + f(ctx, now) + } + } +} diff --git a/internal/internal_test.go b/internal/internal_test.go new file mode 100644 index 00000000..337bbbc0 --- /dev/null +++ b/internal/internal_test.go @@ -0,0 +1,48 @@ +// Copyright 2020 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. + +package internal + +import ( + "context" + "testing" + "time" +) + +func TestPeriodicallyDo(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + didWork := make(chan time.Time, 2) + done := make(chan interface{}) + go func() { + PeriodicallyDo(ctx, 100*time.Millisecond, func(ctx context.Context, t time.Time) { + select { + case didWork <- t: + default: + // No need to assert that we can't send, we just care that we sent. + } + }) + close(done) + }() + + select { + case <-time.After(5 * time.Second): + t.Error("PeriodicallyDo() never called f, wanted at least one call") + case <-didWork: + // PeriodicallyDo called f successfully. + } + + select { + case <-done: + t.Errorf("PeriodicallyDo() finished early, wanted it to still be looping") + case <-didWork: + cancel() + } + + select { + case <-time.After(time.Second): + t.Fatal("PeriodicallyDo() never returned, wanted return after context cancellation") + case <-done: + // PeriodicallyDo successfully returned. + } +} diff --git a/sandbox/metrics.go b/sandbox/metrics.go index 9ec454de..3d800660 100644 --- a/sandbox/metrics.go +++ b/sandbox/metrics.go @@ -13,6 +13,7 @@ import ( "contrib.go.opencensus.io/exporter/prometheus" "contrib.go.opencensus.io/exporter/stackdriver" "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/stats" "go.opencensus.io/stats/view" "go.opencensus.io/tag" mrpb "google.golang.org/genproto/googleapis/api/monitoredres" @@ -22,6 +23,32 @@ import ( // * The views are prefixed with go-playground-sandbox. // * ochttp.KeyServerRoute is added as a tag to label metrics per-route. var ( + mContainers = stats.Int64("go-playground/sandbox/container_count", "number of sandbox containers", stats.UnitDimensionless) + mUnwantedContainers = stats.Int64("go-playground/sandbox/unwanted_container_count", "number of sandbox containers that are unexpectedly running", stats.UnitDimensionless) + mMaxContainers = stats.Int64("go-playground/sandbox/max_container_count", "target number of sandbox containers", stats.UnitDimensionless) + + containerCount = &view.View{ + Name: "go-playground/sandbox/container_count", + Description: "Number of running sandbox containers", + TagKeys: nil, + Measure: mContainers, + Aggregation: view.LastValue(), + } + unwantedContainerCount = &view.View{ + Name: "go-playground/sandbox/unwanted_container_count", + Description: "Number of running sandbox containers that are not being tracked by the sandbox", + TagKeys: nil, + Measure: mUnwantedContainers, + Aggregation: view.LastValue(), + } + maxContainerCount = &view.View{ + Name: "go-playground/sandbox/max_container_count", + Description: "Maximum number of containers to create", + TagKeys: nil, + Measure: mMaxContainers, + Aggregation: view.LastValue(), + } + ServerRequestCountView = &view.View{ Name: "go-playground-sandbox/http/server/request_count", Description: "Count of HTTP requests started", @@ -72,6 +99,9 @@ var ( // When the sandbox is not running on GCE, it will host metrics through a prometheus HTTP handler. func newMetricService() (*metricService, error) { err := view.Register( + containerCount, + unwantedContainerCount, + maxContainerCount, ServerRequestCountView, ServerRequestBytesView, ServerResponseBytesView, diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index f2a49e78..59ba51a5 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -11,6 +11,7 @@ package main import ( + "bufio" "bytes" "context" "crypto/rand" @@ -31,6 +32,7 @@ import ( "time" "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/stats" "go.opencensus.io/trace" "golang.org/x/playground/internal" "golang.org/x/playground/sandbox/sandboxtypes" @@ -146,6 +148,9 @@ func main() { mux.Handle("/run", ochttp.WithRouteTag(http.HandlerFunc(runHandler), "/run")) go makeWorkers() + go internal.PeriodicallyDo(context.Background(), 10*time.Second, func(ctx context.Context, _ time.Time) { + countDockerContainers(ctx) + }) trace.ApplyConfig(trace.Config{DefaultSampler: trace.NeverSample()}) httpServer = &http.Server{ @@ -155,6 +160,70 @@ func main() { log.Fatal(httpServer.ListenAndServe()) } +// dockerContainer is the structure of each line output from docker ps. +type dockerContainer struct { + // ID is the docker container ID. + ID string `json:"ID"` + // Image is the docker image name. + Image string `json:"Image"` + // Names is the docker container name. + Names string `json:"Names"` +} + +// countDockerContainers records the metric for the current number of docker containers. +// It also records the count of any unwanted containers. +func countDockerContainers(ctx context.Context) { + cs, err := listDockerContainers(ctx) + if err != nil { + log.Printf("Error counting docker containers: %v", err) + } + stats.Record(ctx, mContainers.M(int64(len(cs)))) + var unwantedCount int64 + for _, c := range cs { + if c.Names != "" && !isContainerWanted(c.Names) { + unwantedCount++ + } + } + stats.Record(ctx, mUnwantedContainers.M(unwantedCount)) +} + +// listDockerContainers returns the current running play_run containers reported by docker. +func listDockerContainers(ctx context.Context) ([]dockerContainer, error) { + out := new(bytes.Buffer) + cmd := exec.Command("docker", "ps", "--quiet", "--filter", "name=play_run_", "--format", "{{json .}}") + cmd.Stdout, cmd.Stderr = out, out + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("listDockerContainers: cmd.Start() failed: %w", err) + } + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + if err := internal.WaitOrStop(ctx, cmd, os.Interrupt, 250*time.Millisecond); err != nil { + return nil, fmt.Errorf("listDockerContainers: internal.WaitOrStop() failed: %w", err) + } + return parseDockerContainers(out.Bytes()) +} + +// parseDockerContainers parses the json formatted docker output from docker ps. +// +// If there is an error scanning the input, or non-JSON output is encountered, an error is returned. +func parseDockerContainers(b []byte) ([]dockerContainer, error) { + // Parse the output to ensure it is well-formatted in the structure we expect. + var containers []dockerContainer + // Each output line is it's own JSON object, so unmarshal one line at a time. + scanner := bufio.NewScanner(bytes.NewReader(b)) + for scanner.Scan() { + var do dockerContainer + if err := json.Unmarshal(scanner.Bytes(), &do); err != nil { + return nil, fmt.Errorf("parseDockerContainers: error parsing docker ps output: %w", err) + } + containers = append(containers, do) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("parseDockerContainers: error reading docker ps output: %w", err) + } + return containers, nil +} + func handleSignals() { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT) @@ -292,8 +361,10 @@ func runInGvisor() { } func makeWorkers() { + ctx := context.Background() + stats.Record(ctx, mMaxContainers.M(int64(*numWorkers))) for { - c, err := startContainer(context.Background()) + c, err := startContainer(ctx) if err != nil { log.Printf("error starting container: %v", err) time.Sleep(5 * time.Second) @@ -332,6 +403,12 @@ func setContainerWanted(name string, wanted bool) { } } +func isContainerWanted(name string) bool { + wantedMu.Lock() + defer wantedMu.Unlock() + return containerWanted[name] +} + func getContainer(ctx context.Context) (*Container, error) { select { case c := <-readyContainer: diff --git a/sandbox/sandbox_test.go b/sandbox/sandbox_test.go index c5ea09a0..fac52bb2 100644 --- a/sandbox/sandbox_test.go +++ b/sandbox/sandbox_test.go @@ -10,6 +10,8 @@ import ( "strings" "testing" "testing/iotest" + + "github.com/google/go-cmp/cmp" ) func TestLimitedWriter(t *testing.T) { @@ -182,3 +184,43 @@ func TestSwitchWriterMultipleWrites(t *testing.T) { t.Errorf("dst2.Bytes() = %q, wanted %q", dst2.Bytes(), " and this is after") } } + +func TestParseDockerContainers(t *testing.T) { + cases := []struct { + desc string + output string + want []dockerContainer + wantErr bool + }{ + { + desc: "normal output (container per line)", + output: `{"Command":"\"/usr/local/bin/play…\"","CreatedAt":"2020-04-23 17:44:02 -0400 EDT","ID":"f7f170fde076","Image":"gcr.io/golang-org/playground-sandbox-gvisor:latest","Labels":"","LocalVolumes":"0","Mounts":"","Names":"play_run_a02cfe67","Networks":"none","Ports":"","RunningFor":"8 seconds ago","Size":"0B","Status":"Up 7 seconds"} +{"Command":"\"/usr/local/bin/play…\"","CreatedAt":"2020-04-23 17:44:02 -0400 EDT","ID":"af872e55a773","Image":"gcr.io/golang-org/playground-sandbox-gvisor:latest","Labels":"","LocalVolumes":"0","Mounts":"","Names":"play_run_0a69c3e8","Networks":"none","Ports":"","RunningFor":"8 seconds ago","Size":"0B","Status":"Up 7 seconds"}`, + want: []dockerContainer{ + {ID: "f7f170fde076", Image: "gcr.io/golang-org/playground-sandbox-gvisor:latest", Names: "play_run_a02cfe67"}, + {ID: "af872e55a773", Image: "gcr.io/golang-org/playground-sandbox-gvisor:latest", Names: "play_run_0a69c3e8"}, + }, + wantErr: false, + }, + { + desc: "empty output", + wantErr: false, + }, + { + desc: "malformatted output", + output: `xyzzy{}`, + wantErr: true, + }, + } + for _, tc := range cases { + t.Run(tc.desc, func(t *testing.T) { + cs, err := parseDockerContainers([]byte(tc.output)) + if (err != nil) != tc.wantErr { + t.Errorf("parseDockerContainers(_) = %v, %v, wantErr: %v", cs, err, tc.wantErr) + } + if diff := cmp.Diff(tc.want, cs); diff != "" { + t.Errorf("parseDockerContainers() mismatch (-want +got):\n%s", diff) + } + }) + } +} From 26d50f8632ff6fb9d62c407b95c945e3150ac8fa Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Fri, 24 Apr 2020 14:53:31 -0400 Subject: [PATCH 069/148] sandbox: correct stackdriver labels The generic_task target has a required label format that was not being met. Moves metadata fetching into a helper function. Removes call to view.RegisterExporter for StackDriver exporter, which was unncessary. Updates golang/go#25224 Updates golang/go#38530 Change-Id: Ib009f5ce906f5b9479cdda8c7e8322d06e3036e4 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229958 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Carlos Amedee --- sandbox/metrics.go | 78 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 16 deletions(-) diff --git a/sandbox/metrics.go b/sandbox/metrics.go index 3d800660..82b43291 100644 --- a/sandbox/metrics.go +++ b/sandbox/metrics.go @@ -5,8 +5,10 @@ package main import ( + "errors" "fmt" "net/http" + "path" "time" "cloud.google.com/go/compute/metadata" @@ -126,26 +128,14 @@ func newMetricService() (*metricService, error) { if err != nil { return nil, err } - zone, err := metadata.Zone() - if err != nil { - return nil, err - } - iname, err := metadata.InstanceName() + gr, err := gceResource("go-playground-sandbox") if err != nil { return nil, err } sd, err := stackdriver.NewExporter(stackdriver.Options{ - ProjectID: projID, - MonitoredResource: (*monitoredResource)(&mrpb.MonitoredResource{ - Type: "generic_task", - Labels: map[string]string{ - "instance_id": iname, - "job": "go-playground-sandbox", - "project_id": projID, - "zone": zone, - }, - }), + ProjectID: projID, + MonitoredResource: gr, ReportingInterval: time.Minute, // Minimum interval for stackdriver is 1 minute. }) if err != nil { @@ -154,7 +144,6 @@ func newMetricService() (*metricService, error) { // Minimum interval for stackdriver is 1 minute. view.SetReportingPeriod(time.Minute) - view.RegisterExporter(sd) // Start the metrics exporter. if err := sd.StartMetricsExporter(); err != nil { return nil, err @@ -163,6 +152,7 @@ func newMetricService() (*metricService, error) { return &metricService{sdExporter: sd}, nil } +// metricService controls metric exporters. type metricService struct { sdExporter *stackdriver.Exporter pExporter *prometheus.Exporter @@ -176,6 +166,7 @@ func (m *metricService) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } +// Stop flushes metrics and stops exporting. Stop should be called before exiting. func (m *metricService) Stop() { if sde := m.sdExporter; sde != nil { // Flush any unsent data before exiting. @@ -192,3 +183,58 @@ type monitoredResource mrpb.MonitoredResource func (r *monitoredResource) MonitoredResource() (resType string, labels map[string]string) { return r.Type, r.Labels } + +// gceResource populates a monitoredResource with GCE Metadata. +// +// The returned monitoredResource will have the type set to "generic_task". +func gceResource(jobName string) (*monitoredResource, error) { + projID, err := metadata.ProjectID() + if err != nil { + return nil, err + } + zone, err := metadata.Zone() + if err != nil { + return nil, err + } + iname, err := metadata.InstanceName() + if err != nil { + return nil, err + } + igName, err := instanceGroupName() + if err != nil { + return nil, err + } else if igName == "" { + igName = projID + } + + return (*monitoredResource)(&mrpb.MonitoredResource{ + Type: "generic_task", // See: https://cloud.google.com/monitoring/api/resources#tag_generic_task + Labels: map[string]string{ + "project_id": projID, + "location": zone, + "namespace": igName, + "job": jobName, + "task_id": iname, + }, + }), nil +} + +// instanceGroupName fetches the instanceGroupName from the instance metadata. +// +// The instance group manager applies a custom "created-by" attribute to the instance, which is not part of the +// metadata package API, and must be queried separately. +// +// An empty string will be returned if a metadata.NotDefinedError is returned when fetching metadata. +// An error will be returned if other errors occur when fetching metadata. +func instanceGroupName() (string, error) { + ig, err := metadata.InstanceAttributeValue("created-by") + if nde := metadata.NotDefinedError(""); err != nil && !errors.As(err, &nde) { + return "", err + } + if ig == "" { + return "", nil + } + // "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}} + ig = path.Base(ig) + return ig, err +} From 66a9e4bf5e9517c4eaddb8b4af1660910f8817b5 Mon Sep 17 00:00:00 2001 From: Alexander Rakoczy Date: Sat, 25 Apr 2020 10:13:27 -0400 Subject: [PATCH 070/148] sandbox: add container creation metrics This change measures the latency and success of container creation. These metrics will help capacity planning and investigating production issues. Updates golang/go#25224 Updates golang/go#38530 Change-Id: Id7f373acb8741d4465c6e632badb188b6e855787 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229980 Run-TryBot: Alexander Rakoczy TryBot-Result: Gobot Gobot Reviewed-by: Dmitri Shuralyov --- sandbox/metrics.go | 31 +++++++++++++++++++++++++------ sandbox/sandbox.go | 12 ++++++++++++ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/sandbox/metrics.go b/sandbox/metrics.go index 82b43291..075be969 100644 --- a/sandbox/metrics.go +++ b/sandbox/metrics.go @@ -21,13 +21,12 @@ import ( mrpb "google.golang.org/genproto/googleapis/api/monitoredres" ) -// Customizations of ochttp views. Views are updated as follows: -// * The views are prefixed with go-playground-sandbox. -// * ochttp.KeyServerRoute is added as a tag to label metrics per-route. var ( - mContainers = stats.Int64("go-playground/sandbox/container_count", "number of sandbox containers", stats.UnitDimensionless) - mUnwantedContainers = stats.Int64("go-playground/sandbox/unwanted_container_count", "number of sandbox containers that are unexpectedly running", stats.UnitDimensionless) - mMaxContainers = stats.Int64("go-playground/sandbox/max_container_count", "target number of sandbox containers", stats.UnitDimensionless) + kContainerCreateSuccess = tag.MustNewKey("go-playground/sandbox/container_create_success") + mContainers = stats.Int64("go-playground/sandbox/container_count", "number of sandbox containers", stats.UnitDimensionless) + mUnwantedContainers = stats.Int64("go-playground/sandbox/unwanted_container_count", "number of sandbox containers that are unexpectedly running", stats.UnitDimensionless) + mMaxContainers = stats.Int64("go-playground/sandbox/max_container_count", "target number of sandbox containers", stats.UnitDimensionless) + mContainerCreateLatency = stats.Float64("go-playground/sandbox/container_create_latency", "", stats.UnitMilliseconds) containerCount = &view.View{ Name: "go-playground/sandbox/container_count", @@ -50,7 +49,25 @@ var ( Measure: mMaxContainers, Aggregation: view.LastValue(), } + containerCreateCount = &view.View{ + Name: "go-playground/sandbox/container_create_count", + Description: "Number of containers created", + Measure: mContainerCreateLatency, + TagKeys: []tag.Key{kContainerCreateSuccess}, + Aggregation: view.Count(), + } + containerCreationLatency = &view.View{ + Name: "go-playground/sandbox/container_create_latency", + Description: "Latency distribution of container creation", + Measure: mContainerCreateLatency, + Aggregation: ochttp.DefaultLatencyDistribution, + } +) +// Customizations of ochttp views. Views are updated as follows: +// * The views are prefixed with go-playground-sandbox. +// * ochttp.KeyServerRoute is added as a tag to label metrics per-route. +var ( ServerRequestCountView = &view.View{ Name: "go-playground-sandbox/http/server/request_count", Description: "Count of HTTP requests started", @@ -104,6 +121,8 @@ func newMetricService() (*metricService, error) { containerCount, unwantedContainerCount, maxContainerCount, + containerCreateCount, + containerCreationLatency, ServerRequestCountView, ServerRequestBytesView, ServerResponseBytesView, diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go index 59ba51a5..350eeea0 100644 --- a/sandbox/sandbox.go +++ b/sandbox/sandbox.go @@ -33,6 +33,7 @@ import ( "go.opencensus.io/plugin/ochttp" "go.opencensus.io/stats" + "go.opencensus.io/tag" "go.opencensus.io/trace" "golang.org/x/playground/internal" "golang.org/x/playground/sandbox/sandboxtypes" @@ -419,6 +420,17 @@ func getContainer(ctx context.Context) (*Container, error) { } func startContainer(ctx context.Context) (c *Container, err error) { + start := time.Now() + defer func() { + status := "success" + if err != nil { + status = "error" + } + // Ignore error. The only error can be invalid tag key or value length, which we know are safe. + _ = stats.RecordWithTags(ctx, []tag.Mutator{tag.Upsert(kContainerCreateSuccess, status)}, + mContainerCreateLatency.M(float64(time.Since(start))/float64(time.Millisecond))) + }() + name := "play_run_" + randHex(8) setContainerWanted(name, true) cmd := exec.Command("docker", "run", From 560694fe2be3366074d38664c0e1b5ef5e5259b2 Mon Sep 17 00:00:00 2001 From: smasher164 Date: Wed, 22 Apr 2020 14:25:14 -0400 Subject: [PATCH 071/148] playground: use flexbox for navbar layout Previously, resizing the viewport until the width of the logo and control elements is greater would cause all of the control elements to disappear. This behavior became exacerbated with the addition of more control elements. Additionally, the alignment and styling of buttons would vary between browsers, particuarly between desktop and mobile. This change uses flexbox for layout in the navbar, instead of floats. Wrapping is now done per-element, instead of the whole control container. All buttons are adjusted to have a unified alignment, and styling is made more consistent across browsers. Fixes golang/go#38482. Change-Id: I1946c4a44da9de9b206b17d4d155ff8422e9ad80 Reviewed-on: https://go-review.googlesource.com/c/playground/+/229419 Reviewed-by: Andrew Bonventre Run-TryBot: Andrew Bonventre TryBot-Result: Gobot Gobot --- edit.html | 50 ++++++++++++++++++++++-------------------------- static/style.css | 50 ++++++++++++++++++++++++------------------------ 2 files changed, 48 insertions(+), 52 deletions(-) diff --git a/edit.html b/edit.html index 719edabd..677c2f42 100644 --- a/edit.html +++ b/edit.html @@ -104,35 +104,31 @@