diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..3fa8c86b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.terraform diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 15167cd7..00000000 --- a/AUTHORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code refers to The Go Authors for copyright purposes. -# The master list of authors is in the main Go distribution, -# visible at http://tip.golang.org/AUTHORS. diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 1c4577e9..00000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,3 +0,0 @@ -# This source code was written by the Go contributors. -# The master list of contributors is in the main Go distribution, -# visible at http://tip.golang.org/CONTRIBUTORS. diff --git a/Dockerfile b/Dockerfile index 41a0849a..a6c78c6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,213 +1,113 @@ # 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 -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.10.3 -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/ +# The playground builds Go from a bootstrap version because +# the playground deployment is triggered before the artifacts are +# published for the latest version of Go. -RUN apt-get update && apt-get install -y ${BUILD_DEPS} --no-install-recommends +# GO_VERSION is provided by Cloud Build, and is set to the latest +# version of Go. See the configuration in the deploy directory. +ARG GO_VERSION=go1.22.6 + +# GO_BOOTSTRAP_VERSION is downloaded below and used to bootstrap the build from +# source. Therefore, this should be a version that is guaranteed to have +# published artifacts, such as the latest minor of the previous major Go +# release. +# +# See also https://go.dev/issue/69238. +ARG GO_BOOTSTRAP_VERSION=go1.22.6 -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 +############################################################################ +# Build Go at GO_VERSION, and build faketime standard library. +FROM debian:trixie AS build-go +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 -# 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 +ENV GOPATH /go +ENV GOROOT_BOOTSTRAP=/usr/local/go-bootstrap + +# https://docs.docker.com/reference/dockerfile/#understand-how-arg-and-from-interact +ARG GO_VERSION +ENV GO_VERSION ${GO_VERSION} +ARG GO_BOOTSTRAP_VERSION +ENV GO_BOOTSTRAP_VERSION ${GO_BOOTSTRAP_VERSION} + +# Get a bootstrap version of Go for building GO_VERSION. At the time +# of this Dockerfile being built, GO_VERSION's artifacts may not yet +# be published. +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 -# 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 /dev/null || git fetch -q origin $REV) && git reset --hard $REV) - -# Repo github.com/bradfitz/gomemcache at 1952afa (2017-02-08) -ENV REV=1952afaa557dc08e8e0d89eafab110fb501c1a2b -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 -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 &&\ - (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 -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 -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 -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 -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) - -# Repo google.golang.org/api at ab90adb (2018-02-22) -ENV REV=ab90adb3efa287b869ecb698db42f923cc734972 -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` &&\ - (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` &&\ - (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/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 \ - golang.org/x/net/context \ - golang.org/x/net/context/ctxhttp \ - 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/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/godoc/static \ - golang.org/x/tools/imports \ - 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/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/codes \ - google.golang.org/grpc/connectivity \ - google.golang.org/grpc/credentials \ - 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/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 \ - google.golang.org/grpc/transport -# END deps - -# Add and compile playground daemon +RUN mkdir -p $GOROOT_BOOTSTRAP +RUN tar --strip=1 -C $GOROOT_BOOTSTRAP -vxzf /tmp/go.tar.gz + +RUN mkdir /gocache +ENV GOCACHE /gocache +ENV GO111MODULE on +ENV GOPROXY=https://proxy.golang.org + +# Compile Go at target version in /usr/local/go. +WORKDIR /usr/local +RUN git clone https://go.googlesource.com/go go && cd go && git reset --hard $GO_VERSION +WORKDIR /usr/local/go/src +RUN ./make.bash + +############################################################################ +# Build playground web server. +FROM debian:trixie AS build-playground + +RUN apt-get update && apt-get install -y ca-certificates git --no-install-recommends +# Build playground from Go built at GO_VERSION. +COPY --from=build-go /usr/local/go /usr/local/go +ENV GOROOT /usr/local/go +ENV GOPATH /go +ENV PATH="/go/bin:/usr/local/go/bin:${PATH}" +# Cache dependencies for efficient Dockerfile building. +COPY go.mod /go/src/playground/go.mod +COPY go.sum /go/src/playground/go.sum +WORKDIR /go/src/playground +RUN go mod download + +# Add and compile playground daemon. COPY . /go/src/playground/ -RUN go install playground +RUN go install -FROM debian:jessie +############################################################################ +# Final stage. +FROM debian:trixie 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 +# Make a copy in /usr/local/go-faketime where the standard library +# is installed with -tags=faketime. +COPY --from=build-go /usr/local/go /usr/local/go-faketime +ENV CGO_ENABLED 0 ENV GOPATH /go -ENV PATH /usr/local/go/bin:$GOPATH/bin:$PATH - -# Add and compile tour packages -RUN GOOS=nacl GOARCH=amd64p32 go get \ - golang.org/x/tour/pic \ - golang.org/x/tour/reader \ - golang.org/x/tour/tree \ - golang.org/x/tour/wc \ - golang.org/x/talks/2016/applicative/google && \ - rm -rf $GOPATH/src/golang.org/x/tour/.git && \ - rm -rf $GOPATH/src/golang.org/x/talks/.git - -# Add tour packages under their old import paths (so old snippets still work) -RUN mkdir -p $GOPATH/src/code.google.com/p/go-tour && \ - cp -R $GOPATH/src/golang.org/x/tour/* $GOPATH/src/code.google.com/p/go-tour/ && \ - sed -i 's_// import_// public import_' $(find $GOPATH/src/code.google.com/p/go-tour/ -name *.go) && \ - go install \ - 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 +ENV GOROOT /usr/local/go-faketime +ARG GO_VERSION +ENV GO_VERSION ${GO_VERSION} +ENV PATH="/go/bin:/usr/local/go-faketime/bin:${PATH}" + +WORKDIR /usr/local/go-faketime +# golang/go#57495: install std to warm the build cache. We only set +# GOCACHE=/gocache here to keep it as small as possible, since it must be +# copied on every build. +RUN GOCACHE=/gocache ./bin/go install --tags=faketime std +# Ignore the exit code. go vet std does not pass vet with the faketime +# patches, but it successfully caches results for when we vet user +# snippets. +RUN ./bin/go vet --tags=faketime std || true RUN mkdir /app - -COPY --from=builder /go/bin/playground /app +COPY --from=build-playground /go/bin/playground /app COPY edit.html /app COPY static /app/static +COPY examples /app/examples WORKDIR /app -# Run tests -RUN /app/playground test - EXPOSE 8080 ENTRYPOINT ["/app/playground"] diff --git a/LICENSE b/LICENSE index a2dd15fa..3223ab32 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014 The Go Authors. All rights reserved. +Copyright 2014 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer. copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of Google Inc. nor the names of its + * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/Makefile b/Makefile index e4c4d879..4854fb86 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,49 @@ -.PHONY: update-deps docker test +LATEST_GO := $(shell go run ./cmd/latestgo) -update-deps: - go install golang.org/x/build/cmd/gitlock - gitlock --update=Dockerfile golang.org/x/playground +.PHONY: docker test update-cloudbuild-trigger -docker: Dockerfile - docker build -t playground . +docker: + docker build --build-arg GO_VERSION=$(LATEST_GO) -t golang/playground . -test: docker - go test - docker run --rm 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 --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_gvisor: docker + docker kill sandbox_front_test || true + 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 + +define GOTIP_MESSAGE +Note: deploy/gotip_schedule.yaml must be manually managed with gcloud. +Example: + gcloud scheduler jobs update http \ + projects/golang-org/locations/us-central1/jobs/playground-deploy-gotip-playground-schedule \ + --schedule="0 10 * * *" --time-zone="America/New_York" +endef +export GOTIP_MESSAGE + +push-cloudbuild-triggers: + gcloud beta builds triggers import --project golang-org --source deploy/go_trigger.yaml + gcloud beta builds triggers import --project golang-org --source deploy/go_goprev_trigger.yaml + gcloud beta builds triggers import --project golang-org --source deploy/playground_trigger.yaml + gcloud beta builds triggers import --project golang-org --source deploy/playground_goprev_trigger.yaml + gcloud alpha builds triggers import --project golang-org --source deploy/gotip_scheduled_trigger.yaml + @echo "$$GOTIP_MESSAGE" + +pull-cloudbuild-triggers: + gcloud beta builds triggers export --project golang-org playground-redeploy-go-release --destination deploy/go_trigger.yaml + gcloud beta builds triggers export --project golang-org playground-redeploy-goprev-go-release --destination deploy/go_goprev_trigger.yaml + gcloud beta builds triggers export --project golang-org playground-redeploy-playground --destination deploy/playground_trigger.yaml + gcloud beta builds triggers export --project golang-org playground-redeploy-goprev-playground --destination deploy/playground_goprev_trigger.yaml + gcloud alpha builds triggers export --project golang-org playground-deploy-gotip-playground --destination deploy/gotip_scheduled_trigger.yaml + gcloud scheduler --project=golang-org jobs describe --format=yaml playground-deploy-gotip-playground-schedule > deploy/gotip_schedule.yaml diff --git a/README.md b/README.md index 63b05fa2..507145ce 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,84 @@ # playground +[](https://pkg.go.dev/golang.org/x/playground) + This subrepository holds the source for the Go playground: -https://play.golang.org/ +https://go.dev/play/ ## Building -``` +```bash # build the image -docker build -t playground . +docker build -t golang/playground . ``` ## Running -``` -docker run --name=play --rm -d -p 8080:8080 playground +```bash +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 ``` -# Deployment +To run the "gotip" version of the playground, set `GOTIP=true` +in your environment (via `-e GOTIP=true` if using `docker run`). + +## 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.yaml](deploy/go_trigger.yaml), +[deploy/playground_trigger.yaml](deploy/playground_trigger.yaml), +and [deploy/deploy.json](deploy/deploy.json). + +Changes to the trigger configuration can be made to the YAML files, or in the GCP UI, which should be kept in sync +using the `push-cloudbuild-triggers` and `pull-cloudbuild-triggers` make targets. + +### Deploy via Cloud Build + +The Cloud Build configuration will always build and deploy with the latest supported release of Go. + +```bash +gcloud --project=golang-org builds submit --config deploy/deploy.json . ``` -gcloud --project=golang-org --account=person@example.com app deploy app.yaml + +To deploy the "Go tip" version of the playground, which uses the latest +development build, use `deploy_gotip.json` instead: + +```bash +gcloud --project=golang-org builds submit --config deploy/deploy_gotip.json . ``` -# Contributing +### 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): + +```bash +gcloud config set app/cloud_build_timeout 1200 # 20 mins +``` + +Alternatively, to avoid Cloud Build and build locally: + +```bash +make docker +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 +``` + +Then: + +```bash +gcloud --project=golang-org --account=you@google.com app deploy app.yaml +``` + +## Contributing To submit changes to this repository, see -https://golang.org/doc/contribute.html. +https://go.dev/doc/contribute. + +The git repository is https://go.googlesource.com/playground. diff --git a/app.go2go.yaml b/app.go2go.yaml new file mode 100644 index 00000000..3e70e0ea --- /dev/null +++ b/app.go2go.yaml @@ -0,0 +1,10 @@ +runtime: go116 +service: go2goplay +main: ./cmd/redirect + +handlers: +- url: /.* + script: auto + +env_variables: + PLAY_REDIRECT: "https://gotipplay.golang.org" diff --git a/app.goprev.yaml b/app.goprev.yaml new file mode 100644 index 00000000..bb5fc476 --- /dev/null +++ b/app.goprev.yaml @@ -0,0 +1,20 @@ +service: goprevplay +runtime: custom +env: flex + +network: + name: projects/golang-org/global/networks/golang + +resources: + cpu: 2 + memory_gb: 2 + +automatic_scaling: + min_num_instances: 5 + +readiness_check: + path: "/_ah/health" + check_interval_sec: 10 + +env_variables: + MEMCACHED_ADDR: 'memcached-play-golang:11211' diff --git a/app.gotip.yaml b/app.gotip.yaml new file mode 100644 index 00000000..9f8b227b --- /dev/null +++ b/app.gotip.yaml @@ -0,0 +1,22 @@ +service: gotipplay +runtime: custom +env: flex + +network: + name: projects/golang-org/global/networks/golang + +resources: + cpu: 8 + memory_gb: 8 + +automatic_scaling: + min_num_instances: 5 + +readiness_check: + path: "/_ah/health" + check_interval_sec: 10 + +env_variables: + MEMCACHED_ADDR: 'memcached-play-golang:11211' + GOTIP: "true" + diff --git a/app.yaml b/app.yaml index c6791f8f..8790e8a9 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,5 @@ readiness_check: check_interval_sec: 10 env_variables: - MEMCACHED_ADDR: 'memcached:11211' + MEMCACHED_ADDR: 'memcached-play-golang:11211' + 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/client.go b/client.go index c168c3c4..f7a8795f 100644 --- a/client.go +++ b/client.go @@ -1,4 +1,4 @@ -// +build ignore +//go:build ignore // Copyright 2014 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style @@ -8,13 +8,13 @@ package main import ( "encoding/json" - "io/ioutil" + "io" "log" "os" ) func main() { - body, err := ioutil.ReadAll(os.Stdin) + body, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("error reading stdin: %v", err) } diff --git a/cmd/latestgo/main.go b/cmd/latestgo/main.go new file mode 100644 index 00000000..1b4f67bb --- /dev/null +++ b/cmd/latestgo/main.go @@ -0,0 +1,151 @@ +// 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" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "net/http" + "sort" + "strings" + "time" + + "golang.org/x/build/gerrit" + "golang.org/x/build/maintner/maintnerd/maintapi/version" +) + +var ( + prev = flag.Bool("prev", false, "if set, query the previous Go release rather than the last (e.g. 1.17 versus 1.18)") + toolchain = flag.Bool("toolchain", false, "if set, query released toolchains, rather than gerrit tags; toolchains may lag behind gerrit") +) + +func main() { + flag.Parse() + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + var latest []string + if *toolchain { + latest = latestToolchainVersions(ctx) + } else { + client := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth) + latest = latestGerritVersions(ctx, client) + } + if len(latest) < 2 { + log.Fatalf("found %d versions, need at least 2", len(latest)) + } + + if *prev { + fmt.Println(latest[1]) + } else { + fmt.Println(latest[0]) + } +} + +// latestGerritVersions queries the latest versions for each major Go release, +// among Gerrit tags. +func latestGerritVersions(ctx context.Context, client *gerrit.Client) []string { + tagInfo, err := client.GetProjectTags(ctx, "go") + if err != nil { + log.Fatalf("error retrieving project tags for 'go': %v", err) + } + + if len(tagInfo) == 0 { + log.Fatalln("no project tags found for 'go'") + } + + var tags []string + for _, tag := range tagInfo { + tags = append(tags, strings.TrimPrefix(tag.Ref, "refs/tags/")) + } + return latestPatches(tags) +} + +// latestToolchainVersions queries the latest versions for each major Go +// release, among published toolchains. It may have fewer versions than +// [latestGerritVersions], because not all toolchains may be published. +func latestToolchainVersions(ctx context.Context) []string { + req, err := http.NewRequestWithContext(ctx, "GET", "https://go.dev/dl/?mode=json", nil) + if err != nil { + log.Fatalf("NewRequest: %v", err) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalf("fetching toolchains: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + log.Fatalf("fetching toolchains: got status %d, want 200", res.StatusCode) + } + data, err := io.ReadAll(res.Body) + if err != nil { + log.Fatalf("reading body: %v", err) + } + + type release struct { + Version string `json:"version"` + } + var releases []release + if err := json.Unmarshal(data, &releases); err != nil { + log.Fatalf("unmarshaling releases JSON: %v", err) + } + var all []string + for _, rel := range releases { + all = append(all, rel.Version) + } + return latestPatches(all) +} + +// latestPatches returns the latest minor release of each major Go version, +// among the set of tag or tag-like strings. The result is in descending +// order, such that later versions are sorted first. +// +// Tags that aren't of the form goX, goX.Y, or goX.Y.Z are ignored. +func latestPatches(tags []string) []string { + // Find the latest patch version for each major Go version. + type majMin struct { + maj, min int // maj, min in semver terminology, which corresponds to a major go release + } + type patchTag struct { + patch int + tag string // Go repo tag for this version + } + latestPatches := make(map[majMin]patchTag) // (maj, min) -> latest patch info + + for _, tag := range tags { + maj, min, patch, ok := version.ParseTag(tag) + if !ok { + continue + } + mm := majMin{maj, min} + if latest, ok := latestPatches[mm]; !ok || latest.patch < patch { + latestPatches[mm] = patchTag{patch, tag} + } + } + + var mms []majMin + for mm := range latestPatches { + mms = append(mms, mm) + } + // Sort by descending semantic ordering, so that later versions are first. + sort.Slice(mms, func(i, j int) bool { + if mms[i].maj != mms[j].maj { + return mms[i].maj > mms[j].maj + } + return mms[i].min > mms[j].min + }) + + var latest []string + for _, mm := range mms { + latest = append(latest, latestPatches[mm].tag) + } + return latest +} diff --git a/cmd/redirect/main.go b/cmd/redirect/main.go new file mode 100644 index 00000000..2292b8eb --- /dev/null +++ b/cmd/redirect/main.go @@ -0,0 +1,32 @@ +// Copyright 2021 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. + +// redirect serves an http server that redirects to the URL specified by the +// environment variable PLAY_REDIRECT. +package main + +import ( + "log" + "net/http" + "os" +) + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + redirect := os.Getenv("PLAY_REDIRECT") + if redirect == "" { + redirect = "https://play.golang.org" + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, redirect+r.URL.Path, http.StatusFound) + }) + + log.Printf("Listening on :%v ...", port) + log.Fatalf("Error listening on :%v: %v", port, http.ListenAndServe(":"+port, handler)) +} diff --git a/deploy/deploy.json b/deploy/deploy.json new file mode 100644 index 00000000..db7ca68a --- /dev/null +++ b/deploy/deploy.json @@ -0,0 +1,57 @@ +{ + "steps": [ + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/playground/cmd/latestgo > /workspace/goversion && echo GO_VERSION=`cat /workspace/goversion`" + ] + }, + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/playground/cmd/latestgo -prev -toolchain > /workspace/gobootstrapversion && echo GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion`" + ] + }, + { + "name": "gcr.io/cloud-builders/docker", + "entrypoint": "sh", + "args": [ + "-c", + "docker build --build-arg GO_VERSION=`cat /workspace/goversion` --build-arg GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion` -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" + ] + }, + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/website/cmd/versionprune@latest -dry_run=false -project=$PROJECT_ID -service=play" + ] + } + ], + "timeout": "3600s", + "options": { + "machineType": "N1_HIGHCPU_8" + } +} diff --git a/deploy/deploy_goprev.json b/deploy/deploy_goprev.json new file mode 100644 index 00000000..3ba1ec3b --- /dev/null +++ b/deploy/deploy_goprev.json @@ -0,0 +1,57 @@ +{ + "steps": [ + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/playground/cmd/latestgo -prev > /workspace/goversion && echo GO_VERSION=`cat /workspace/goversion`" + ] + }, + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/playground/cmd/latestgo -prev -toolchain > /workspace/gobootstrapversion && echo GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion`" + ] + }, + { + "name": "gcr.io/cloud-builders/docker", + "entrypoint": "sh", + "args": [ + "-c", + "docker build --build-arg GO_VERSION=`cat /workspace/goversion` --build-arg GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion` -t gcr.io/$PROJECT_ID/playground-goprev ." + ] + }, + { + "name": "gcr.io/cloud-builders/docker", + "args": [ + "push", + "gcr.io/$PROJECT_ID/playground-goprev" + ] + }, + { + "name": "gcr.io/cloud-builders/gcloud", + "args": [ + "app", + "deploy", + "app.goprev.yaml", + "--project=$PROJECT_ID", + "--image-url=gcr.io/$PROJECT_ID/playground-goprev:latest" + ] + }, + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/website/cmd/versionprune@latest -dry_run=false -project=$PROJECT_ID -service=goprevplay" + ] + } + ], + "timeout": "3600s", + "options": { + "machineType": "N1_HIGHCPU_8" + } +} diff --git a/deploy/deploy_gotip.json b/deploy/deploy_gotip.json new file mode 100644 index 00000000..4eb8b17f --- /dev/null +++ b/deploy/deploy_gotip.json @@ -0,0 +1,49 @@ +{ + "steps": [ + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/playground/cmd/latestgo -prev -toolchain > /workspace/gobootstrapversion && echo GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion`" + ] + }, + { + "name": "gcr.io/cloud-builders/docker", + "entrypoint": "sh", + "args": [ + "-c", + "docker build --build-arg GO_VERSION=master --build-arg GO_BOOTSTRAP_VERSION=`cat /workspace/gobootstrapversion` -t gcr.io/$PROJECT_ID/playground-gotip ." + ] + }, + { + "name": "gcr.io/cloud-builders/docker", + "args": [ + "push", + "gcr.io/$PROJECT_ID/playground-gotip" + ] + }, + { + "name": "gcr.io/cloud-builders/gcloud", + "args": [ + "app", + "deploy", + "app.gotip.yaml", + "--project=$PROJECT_ID", + "--image-url=gcr.io/$PROJECT_ID/playground-gotip:latest" + ] + }, + { + "name": "golang", + "entrypoint": "sh", + "args": [ + "-c", + "go run golang.org/x/website/cmd/versionprune@latest -dry_run=false -project=$PROJECT_ID -service=gotipplay" + ] + } + ], + "timeout": "3600s", + "options": { + "machineType": "N1_HIGHCPU_8" + } +} diff --git a/deploy/go_goprev_trigger.yaml b/deploy/go_goprev_trigger.yaml new file mode 100644 index 00000000..6dbba783 --- /dev/null +++ b/deploy/go_goprev_trigger.yaml @@ -0,0 +1,25 @@ +build: + steps: + - args: + - clone + - --depth + - '1' + - https://go.googlesource.com/playground + name: gcr.io/cloud-builders/git + - args: + - builds + - submit + - --async + - --config + - deploy/deploy_goprev.json + - . + dir: playground + name: gcr.io/cloud-builders/gcloud +createTime: '2022-03-23T14:26:03.683560061Z' +description: Redeploy the goprev playground on new tagged Go release +id: 652aa58f-6376-4a89-a881-e5b0ed2ddbba +name: playground-redeploy-goprev-go-release +triggerTemplate: + projectId: golang-org + repoName: go + tagName: ^go[0-9](\.[0-9]+)+$ diff --git a/deploy/go_trigger.yaml b/deploy/go_trigger.yaml new file mode 100644 index 00000000..48800694 --- /dev/null +++ b/deploy/go_trigger.yaml @@ -0,0 +1,25 @@ +build: + steps: + - args: + - clone + - --depth + - '1' + - https://go.googlesource.com/playground + name: gcr.io/cloud-builders/git + - args: + - builds + - submit + - --async + - --config + - deploy/deploy.json + - . + dir: playground + name: gcr.io/cloud-builders/gcloud +createTime: '2019-06-18T17:59:14.019265678Z' +description: Redeploy playground on new tagged Go release +id: 5a2c9e25-a71a-4adf-a785-76c3eca2ac8a +name: playground-redeploy-go-release +triggerTemplate: + projectId: golang-org + repoName: go + tagName: ^go[0-9](\.[0-9]+)+$ diff --git a/deploy/gotip_schedule.yaml b/deploy/gotip_schedule.yaml new file mode 100644 index 00000000..dcc1900d --- /dev/null +++ b/deploy/gotip_schedule.yaml @@ -0,0 +1,20 @@ +attemptDeadline: 180s +description: Deploy gotip playground daily +httpTarget: + body: eyJicmFuY2hOYW1lIjoibWFzdGVyIn0= + headers: + Content-Type: application/octet-stream + User-Agent: Google-Cloud-Scheduler + httpMethod: POST + oauthToken: + scope: https://www.googleapis.com/auth/cloud-platform + serviceAccountEmail: cloud-build-trigger-scheduler@golang-org.iam.gserviceaccount.com + uri: https://cloudbuild.googleapis.com/v1/projects/golang-org/triggers/b30b6980-4b88-4c10-91d4-5705c793fa1b:run +lastAttemptTime: '2022-03-23T14:00:00.657383Z' +name: projects/golang-org/locations/us-central1/jobs/playground-deploy-gotip-playground-schedule +schedule: 0 10 * * * +scheduleTime: '2022-03-24T14:00:00.140242Z' +state: ENABLED +status: {} +timeZone: America/New_York +userUpdateTime: '2021-11-23T16:27:07Z' diff --git a/deploy/gotip_scheduled_trigger.yaml b/deploy/gotip_scheduled_trigger.yaml new file mode 100644 index 00000000..748e40d4 --- /dev/null +++ b/deploy/gotip_scheduled_trigger.yaml @@ -0,0 +1,13 @@ +createTime: '2021-11-23T16:11:02.136351538Z' +description: Deploy gotip playground daily +gitFileSource: + path: deploy/deploy_gotip.json + repoType: CLOUD_SOURCE_REPOSITORIES + revision: refs/heads/master + uri: https://source.developers.google.com/p/golang-org/r/playground +id: b30b6980-4b88-4c10-91d4-5705c793fa1b +name: playground-deploy-gotip-playground +sourceToBuild: + ref: refs/heads/master + repoType: CLOUD_SOURCE_REPOSITORIES + uri: https://source.developers.google.com/p/golang-org/r/playground diff --git a/deploy/playground_goprev_trigger.yaml b/deploy/playground_goprev_trigger.yaml new file mode 100644 index 00000000..1a255f7a --- /dev/null +++ b/deploy/playground_goprev_trigger.yaml @@ -0,0 +1,9 @@ +createTime: '2022-03-23T14:30:37.556961257Z' +description: Redeploy the goprev playground on x/playground commit +filename: deploy/deploy_goprev.json +id: 4964e99e-4472-4279-8b92-244cbc5e2cd6 +name: playground-redeploy-goprev-playground +triggerTemplate: + branchName: ^master$ + projectId: golang-org + repoName: playground diff --git a/deploy/playground_trigger.yaml b/deploy/playground_trigger.yaml new file mode 100644 index 00000000..bfb75858 --- /dev/null +++ b/deploy/playground_trigger.yaml @@ -0,0 +1,9 @@ +createTime: '2019-07-09T19:50:40.493493139Z' +description: Redeploy playground on x/playground commit +filename: deploy/deploy.json +id: cb46eb75-8665-4365-8e93-b8f7bfbd4807 +name: playground-redeploy-playground +triggerTemplate: + branchName: ^master$ + projectId: golang-org + repoName: playground diff --git a/edit.go b/edit.go index b69c1c0f..4e00d1ed 100644 --- a/edit.go +++ b/edit.go @@ -20,25 +20,33 @@ var editTemplate = template.Must(template.ParseFiles("edit.html")) type editData struct { Snippet *snippet - Share bool Analytics bool GoVersion string + Gotip bool + Examples []example } 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) return } - snip := &snippet{Body: []byte(hello)} + // Serve 404 for /foo. + if r.URL.Path != "/" && !strings.HasPrefix(r.URL.Path, "/p/") { + http.NotFound(w, r) + return + } + + snip := &snippet{Body: []byte(s.examples.hello())} if strings.HasPrefix(r.URL.Path, "/p/") { - if !allowShare(r) { - w.WriteHeader(http.StatusUnavailableForLegalReasons) - w.Write([]byte(`
Viewing and/or sharing code snippets is not available in your country for legal reasons. This message might also appear if your country is misdetected. If you believe this is an error, please file an issue.
`)) - return - } id := r.URL.Path[3:] serveText := false if strings.HasSuffix(id, ".go") { @@ -64,26 +72,22 @@ func (s *server) handleEdit(w http.ResponseWriter, r *http.Request) { return } } + + if r.Host == hostname { + // The main playground is now on go.dev/play. + http.Redirect(w, r, "https://go.dev/play"+r.URL.Path, http.StatusFound) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") data := &editData{ Snippet: snip, - Share: allowShare(r), - Analytics: r.Host == hostname, GoVersion: runtime.Version(), + Gotip: s.gotip, + Examples: s.examples.examples, } if err := editTemplate.Execute(w, data); err != nil { s.log.Errorf("editTemplate.Execute(w, %+v): %v", data, err) return } } - -const hello = `package main - -import ( - "fmt" -) - -func main() { - fmt.Println("Hello, playground") -} -` diff --git a/edit.html b/edit.html index f3cd3be9..422d1f46 100644 --- a/edit.html +++ b/edit.html @@ -1,7 +1,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.
@@ -179,8 +175,12 @@
+{{if .Gotip}}
+This playground uses a development version of Go.
+{{else}}
The playground uses the latest stable release of Go.
-The current version is {{.GoVersion}}.
+{{end}}
+The current version is {{.GoVersion}}.
diff --git a/examples.go b/examples.go
new file mode 100644
index 00000000..d834d1f9
--- /dev/null
+++ b/examples.go
@@ -0,0 +1,153 @@
+// Copyright 2021 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"
+ "os"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strings"
+ "time"
+)
+
+// examplesHandler serves example content out of the examples directory.
+type examplesHandler struct {
+ modtime time.Time
+ examples []example
+}
+
+type example struct {
+ Title string
+ Path string
+ Content string
+}
+
+func (h *examplesHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ for _, e := range h.examples {
+ if e.Path == req.URL.Path {
+ http.ServeContent(w, req, e.Path, h.modtime, strings.NewReader(e.Content))
+ return
+ }
+ }
+ http.NotFound(w, req)
+}
+
+// hello returns the hello text for this instance, which depends on the Go
+// version and whether or not we are serving Gotip examples.
+func (h *examplesHandler) hello() string {
+ return h.examples[0].Content
+}
+
+// newExamplesHandler reads from the examples directory, returning a handler to
+// serve their content.
+//
+// If gotip is set, all files ending in .txt will be included in the set of
+// examples. If gotip is not set, files ending in .gotip.txt are excluded.
+// Examples must start with a line beginning "// Title:" that sets their title.
+//
+// modtime is used for content caching headers.
+func newExamplesHandler(gotip bool, modtime time.Time) (*examplesHandler, error) {
+ const dir = "examples"
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, err
+ }
+
+ var examples []example
+ for _, entry := range entries {
+ name := entry.Name()
+
+ // Read examples ending in .txt, skipping those ending in .gotip.txt if
+ // gotip is not set.
+ prefix := "" // if non-empty, this is a relevant example file
+ if strings.HasSuffix(name, ".gotip.txt") {
+ if gotip {
+ prefix = strings.TrimSuffix(name, ".gotip.txt")
+ }
+ } else if strings.HasSuffix(name, ".txt") {
+ prefix = strings.TrimSuffix(name, ".txt")
+ }
+
+ if prefix == "" {
+ continue
+ }
+
+ data, err := os.ReadFile(filepath.Join(dir, name))
+ if err != nil {
+ return nil, err
+ }
+ content := string(data)
+
+ // Extract the magic "// Title:" comment specifying the example's title.
+ nl := strings.IndexByte(content, '\n')
+ const titlePrefix = "// Title:"
+ if nl == -1 || !strings.HasPrefix(content, titlePrefix) {
+ return nil, fmt.Errorf("malformed example for %q: must start with a title line beginning %q", name, titlePrefix)
+ }
+ title := strings.TrimPrefix(content[:nl], titlePrefix)
+ title = strings.TrimSpace(title)
+
+ examples = append(examples, example{
+ Title: title,
+ Path: name,
+ Content: content[nl+1:],
+ })
+ }
+
+ // Sort by title, before prepending the hello example (we always want Hello
+ // to be first).
+ sort.Slice(examples, func(i, j int) bool {
+ return examples[i].Title < examples[j].Title
+ })
+
+ // For Gotip, serve hello content that includes the Go version.
+ hi := hello
+ if gotip {
+ hi = helloGotip
+ }
+
+ examples = append([]example{
+ {"Hello, playground", "hello.txt", hi},
+ }, examples...)
+ return &examplesHandler{
+ modtime: modtime,
+ examples: examples,
+ }, nil
+}
+
+const hello = `package main
+
+import (
+ "fmt"
+)
+
+func main() {
+ fmt.Println("Hello, playground")
+}
+`
+
+var helloGotip = fmt.Sprintf(`package main
+
+import (
+ "fmt"
+)
+
+// This playground uses a development build of Go:
+// %s
+
+func Print[T any](s ...T) {
+ for _, v := range s {
+ fmt.Print(v)
+ }
+}
+
+func main() {
+ Print("Hello, ", "playground\n")
+}
+`, runtime.Version())
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..1021ad30
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,9 @@
+# Playground Examples
+
+Add examples to the playground by adding files to this directory with the
+`.txt` file extension. Examples with file names ending in `.gotip.txt` are only
+displayed on the gotip playground.
+
+Each example must start with a line beginning with "// Title:", specifying the
+title of the example in the selection menu. This title line will be stripped
+from the example before serving.
diff --git a/examples/clear.txt b/examples/clear.txt
new file mode 100644
index 00000000..26c767a6
--- /dev/null
+++ b/examples/clear.txt
@@ -0,0 +1,19 @@
+// Title: Clear
+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/http.txt b/examples/http.txt
new file mode 100644
index 00000000..d35788a2
--- /dev/null
+++ b/examples/http.txt
@@ -0,0 +1,37 @@
+// Title: HTTP server
+package main
+
+import (
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "net/http"
+ "os"
+)
+
+func main() {
+ http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprint(w, "Hello, playground")
+ })
+
+ log.Println("Starting server...")
+ l, err := net.Listen("tcp", "localhost:8080")
+ if err != nil {
+ log.Fatal(err)
+ }
+ go func() {
+ log.Fatal(http.Serve(l, nil))
+ }()
+
+ log.Println("Sending request...")
+ res, err := http.Get("http://localhost:8080/hello")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ log.Println("Reading response...")
+ if _, err := io.Copy(os.Stdout, res.Body); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/examples/image.txt b/examples/image.txt
new file mode 100644
index 00000000..01c6cc1b
--- /dev/null
+++ b/examples/image.txt
@@ -0,0 +1,41 @@
+// Title: Display image
+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/min.gotip.txt b/examples/min.gotip.txt
new file mode 100644
index 00000000..3c719788
--- /dev/null
+++ b/examples/min.gotip.txt
@@ -0,0 +1,19 @@
+// Title: Generic min
+package main
+
+import (
+ "fmt"
+ "constraints"
+)
+
+func min[P constraints.Ordered](x, y P) P {
+ if x < y {
+ return x
+ } else {
+ return y
+ }
+}
+
+func main() {
+ fmt.Println(min(42, 24))
+}
diff --git a/examples/multi.txt b/examples/multi.txt
new file mode 100644
index 00000000..0c2800d9
--- /dev/null
+++ b/examples/multi.txt
@@ -0,0 +1,22 @@
+// Title: Multiple files
+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..6377ab1e
--- /dev/null
+++ b/examples/sleep.txt
@@ -0,0 +1,18 @@
+// Title: Sleep
+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..c31fcda1
--- /dev/null
+++ b/examples/test.txt
@@ -0,0 +1,38 @@
+// Title: Test
+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/fmt.go b/fmt.go
index 09f50f26..98811e4a 100644
--- a/fmt.go
+++ b/fmt.go
@@ -9,8 +9,9 @@ import (
"fmt"
"go/format"
"net/http"
- "strings"
+ "path"
+ "golang.org/x/mod/modfile"
"golang.org/x/tools/imports"
)
@@ -19,26 +20,62 @@ type fmtResponse struct {
Error string
}
-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)
+func (s *server) 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
}
- 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 {
+ 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
+ }
+ fs.AddFile(f, out)
}
- } else {
- resp.Body = string(out)
}
- json.NewEncoder(w).Encode(resp)
+
+ s.writeJSONResponse(w, fmtResponse{Body: string(fs.Format())}, http.StatusOK)
+}
+
+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
new file mode 100644
index 00000000..b6e3f196
--- /dev/null
+++ b/fmt_test.go
@@ -0,0 +1,157 @@
+// 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"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+func TestHandleFmt(t *testing.T) {
+ s, err := newServer(testingOptions(t))
+ if err != nil {
+ t.Fatalf("newServer(testingOptions(t)): %v", err)
+ }
+
+ for _, tt := range []struct {
+ name string
+ method string
+ body string
+ imports bool
+ want string
+ wantErr string
+ }{
+ {
+ 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",
+ method: http.MethodPost,
+ body: "-- prog.go --\n package main",
+ want: "-- prog.go --\npackage main\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",
+ 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",
+ method: http.MethodPost,
+ body: "-- go.mod --\n module \"foo\" ",
+ want: "-- go.mod --\nmodule foo\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",
+ 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",
+ want: "package main\n-- go.mod --\nmodule foo\n-- plain.txt --\n plain text \n\n\n",
+ },
+ {
+ 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",
+ },
+ } {
+ 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")
+ s.handleFmt(rec, req)
+ resp := rec.Result()
+ 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)
+ }
+ 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
new file mode 100644
index 00000000..f799e068
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,57 @@
+module golang.org/x/playground
+
+go 1.24.0
+
+require (
+ cloud.google.com/go/compute/metadata v0.5.0
+ cloud.google.com/go/datastore v1.13.0
+ contrib.go.opencensus.io/exporter/prometheus v0.4.2
+ contrib.go.opencensus.io/exporter/stackdriver v0.13.10
+ github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d
+ github.com/google/go-cmp v0.7.0
+ go.opencensus.io v0.24.0
+ golang.org/x/build v0.0.0-20251007194244-0bced2e98e20
+ golang.org/x/mod v0.29.0
+ golang.org/x/tools v0.38.0
+ golang.org/x/tools/godoc v0.1.0-deprecated
+ google.golang.org/api v0.136.0
+ google.golang.org/appengine v1.6.8-0.20221117013220-504804fb50de
+ google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142
+)
+
+require (
+ cloud.google.com/go v0.110.7 // indirect
+ cloud.google.com/go/monitoring v1.15.1 // indirect
+ cloud.google.com/go/trace v1.10.1 // indirect
+ github.com/aws/aws-sdk-go v1.43.20 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
+ github.com/cespare/xxhash/v2 v2.3.0 // indirect
+ github.com/go-kit/log v0.2.1 // indirect
+ github.com/go-logfmt/logfmt v0.5.1 // indirect
+ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/google/s2a-go v0.1.4 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
+ github.com/googleapis/gax-go/v2 v2.12.0 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+ github.com/prometheus/client_golang v1.13.0 // indirect
+ github.com/prometheus/client_model v0.2.0 // indirect
+ github.com/prometheus/common v0.37.0 // indirect
+ github.com/prometheus/procfs v0.8.0 // indirect
+ github.com/prometheus/statsd_exporter v0.22.7 // indirect
+ golang.org/x/crypto v0.43.0 // indirect
+ golang.org/x/net v0.46.0 // indirect
+ golang.org/x/oauth2 v0.32.0 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/sys v0.37.0 // indirect
+ golang.org/x/text v0.30.0 // indirect
+ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
+ google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f // indirect
+ google.golang.org/grpc v1.67.1 // indirect
+ google.golang.org/protobuf v1.34.2 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 00000000..c39e150b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,773 @@
+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/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/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/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
+cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
+cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
+cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
+cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
+cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
+cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
+cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
+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/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY=
+cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/datastore v1.13.0 h1:ktbC66bOQB3HJPQe8qNI1/aiQ77PMu7hD4mzE6uxe3w=
+cloud.google.com/go/datastore v1.13.0/go.mod h1:KjdB88W897MRITkvWWJrg2OUtrR5XVj1EoLgSp6/N70=
+cloud.google.com/go/monitoring v1.1.0/go.mod h1:L81pzz7HKn14QCMaCs6NTQkdBnE87TElyanS95vIcl4=
+cloud.google.com/go/monitoring v1.15.1 h1:65JhLMd+JiYnXr6j5Z63dUYCuOg770p8a/VC+gil/58=
+cloud.google.com/go/monitoring v1.15.1/go.mod h1:lADlSAlFdbqQuwwpaImhsJXu1QSdd3ojypXrFSMr2rM=
+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/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/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+cloud.google.com/go/trace v1.0.0/go.mod h1:4iErSByzxkyHWzzlAj63/Gmjz0NH1ASqhJguHpGcr6A=
+cloud.google.com/go/trace v1.10.1 h1:EwGdOLCNfYOOPtgqo+D2sDLZmRCEO1AagRTJCU6ztdg=
+cloud.google.com/go/trace v1.10.1/go.mod h1:gbtL94KE5AJLH3y+WVpfWILmqgc6dXcqgNXdOPAQTYk=
+contrib.go.opencensus.io/exporter/prometheus v0.4.2 h1:sqfsYl5GIY/L570iT+l93ehxaWJs2/OwXtiWwew3oAg=
+contrib.go.opencensus.io/exporter/prometheus v0.4.2/go.mod h1:dvEHbiKmgvbr5pjaF9fpw1KeYcjrnC1J8B+JKjsZyRQ=
+contrib.go.opencensus.io/exporter/stackdriver v0.13.10 h1:a9+GZPUe+ONKUwULjlEOucMMG0qfSCCenlji0Nhqbys=
+contrib.go.opencensus.io/exporter/stackdriver v0.13.10/go.mod h1:I5htMbyta491eUxufwwZPQdcKvvgzMB4O9ni41YnIM8=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+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/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/aws/aws-sdk-go v1.43.20 h1:cD1gPqDNCxzuKdANXymOvM+4hXDLCeab/WYn5JnZQB0=
+github.com/aws/aws-sdk-go v1.43.20/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d h1:pVrfxiGfwelyab6n21ZBkbkmbevaf+WvMIiR7sr97hw=
+github.com/bradfitz/gomemcache v0.0.0-20220106215444-fb4bf637b56d/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
+github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+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/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+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/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
+github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+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/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
+github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
+github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+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/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/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/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+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/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+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/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
+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/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc=
+github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
+github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
+github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
+github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
+github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+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/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+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/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
+github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
+github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=
+github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
+github.com/prometheus/common v0.35.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
+github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=
+github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
+github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
+github.com/prometheus/statsd_exporter v0.22.7 h1:7Pji/i2GuhK6Lu7DHrtTkFmNBCudCPT1pX2CziuyQR0=
+github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stvp/go-udp-testing v0.0.0-20201019212854-469649b16807/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+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.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+golang.org/x/build v0.0.0-20251007194244-0bced2e98e20 h1:6O4r6YYe7xNsFnTNd1o6sH1cXJ1VZCbcqA0qo+PC3YQ=
+golang.org/x/build v0.0.0-20251007194244-0bced2e98e20/go.mod h1:166GOek5CoICJKGMVavxt3PcnHdt+u7RV4xkfeFJDbs=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+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/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+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/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/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/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/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/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+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-20181114220301-adae6a3d119a/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-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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/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-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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+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-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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
+golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
+golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
+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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/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/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-20200106162015-b016eb3dc98e/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+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-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/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-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=
+golang.org/x/tools/godoc v0.1.0-deprecated/go.mod h1:qM63CriJ961IHWmnWa9CjZnBndniPt4a3CK0PVB9bIg=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
+golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+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.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/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
+google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
+google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
+google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
+google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
+google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
+google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
+google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E=
+google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
+google.golang.org/api v0.136.0 h1:e/6enzUE1s4tGPa6Q3ZYShKTtvRc+1Jq0rrafhppmOs=
+google.golang.org/api v0.136.0/go.mod h1:XtJfF+V2zgUxelOn5Zs3kECtluMxneJG8ZxUTlLNTPA=
+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/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.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8-0.20221117013220-504804fb50de h1:MvEeYmzkzk0Rsw+ceqy28aIJN7Mum+4aYqBwCMqYNug=
+google.golang.org/appengine v1.6.8-0.20221117013220-504804fb50de/go.mod h1:BbwiCY3WCmCUKOJTrX5NwgQzew1c32w3kxa6Sxvs0cQ=
+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/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/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
+google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
+google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
+google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
+google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
+google.golang.org/genproto v0.0.0-20210921142501-181ce0d877f6/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211018162055-cf77aa76bad2/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
+google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
+google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
+google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f h1:cUMEy+8oS78BWIH9OWazBkzbr090Od9tWBNtZHkOhf0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240930140551-af27646dc61f/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+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.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/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
+google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
+google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
+google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+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/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+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/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/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/internal/gcpdial/gcpdial.go b/internal/gcpdial/gcpdial.go
new file mode 100644
index 00000000..2b310089
--- /dev/null
+++ b/internal/gcpdial/gcpdial.go
@@ -0,0 +1,314 @@
+// 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"
+ "log"
+ "math/rand"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "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(io.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() {
+ // TODO(golang.org/issue/38315) - Plumb a context in here correctly
+ ctx := context.TODO()
+ t := time.NewTicker(10 * time.Second)
+ defer t.Stop()
+ for {
+ d.pollOnce(ctx)
+ select {
+ case <-ctx.Done():
+ return
+ case <-t.C:
+ }
+ }
+}
+
+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()
+ 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
+ }
+ p := newProber(d, instURL)
+ go p.probeLoop()
+ if d.prober == nil {
+ d.prober = map[string]*prober{}
+ }
+ d.prober[instURL] = p
+ }
+ d.lastInstances = res
+}
+
+// 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 that 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..f010c6e5
--- /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 package.
+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/internal/internal.go b/internal/internal.go
new file mode 100644
index 00000000..274bf2c9
--- /dev/null
+++ b/internal/internal.go
@@ -0,0 +1,84 @@
+// 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"
+ "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
+}
+
+// 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/internal/metrics/service.go b/internal/metrics/service.go
new file mode 100644
index 00000000..a19aa268
--- /dev/null
+++ b/internal/metrics/service.go
@@ -0,0 +1,191 @@
+// Copyright 2021 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 metrics provides a service for reporting metrics to
+// Stackdriver, or locally during development.
+package metrics
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "path"
+ "time"
+
+ "cloud.google.com/go/compute/metadata"
+ "contrib.go.opencensus.io/exporter/prometheus"
+ "contrib.go.opencensus.io/exporter/stackdriver"
+ "go.opencensus.io/stats/view"
+ "google.golang.org/appengine"
+ mrpb "google.golang.org/genproto/googleapis/api/monitoredres"
+)
+
+// NewService initializes a *Service.
+//
+// The Service returned is configured to send metric data to
+// StackDriver. When not running on GCE, it will host metrics through
+// a prometheus HTTP handler.
+//
+// views will be passed to view.Register for export to the metric
+// service.
+func NewService(resource *MonitoredResource, views []*view.View) (*Service, error) {
+ err := view.Register(views...)
+ 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("prometheus.NewExporter: %w", err)
+ }
+ view.RegisterExporter(pe)
+ return &Service{pExporter: pe}, nil
+ }
+
+ projID, err := metadata.ProjectID()
+ if err != nil {
+ return nil, err
+ }
+ if resource == nil {
+ return nil, errors.New("resource is required, got nil")
+ }
+ sde, err := stackdriver.NewExporter(stackdriver.Options{
+ ProjectID: projID,
+ MonitoredResource: resource,
+ 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)
+ // Start the metrics exporter.
+ if err := sde.StartMetricsExporter(); err != nil {
+ sde.Close()
+ return nil, err
+ }
+
+ return &Service{sdExporter: sde}, nil
+}
+
+// Service controls metric exporters.
+type Service struct {
+ sdExporter *stackdriver.Exporter
+ pExporter *prometheus.Exporter
+}
+
+func (m *Service) 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)
+}
+
+// Stop flushes metrics and stops exporting. Stop should be called
+// before exiting.
+func (m *Service) 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
+}
+
+// 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
+ }
+ inst, err := metadata.InstanceName()
+ if err != nil {
+ return nil, err
+ }
+ group, err := instanceGroupName()
+ if err != nil {
+ return nil, err
+ } else if group == "" {
+ group = 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": group,
+ "job": jobName,
+ "task_id": inst,
+ },
+ }), nil
+}
+
+// GAEResource returns a *MonitoredResource with fields populated and
+// for StackDriver.
+//
+// The resource will be in StackDrvier's gae_instance type.
+func GAEResource(ctx context.Context) (*MonitoredResource, error) {
+ // appengine.IsAppEngine is confusingly false as we're using a custom
+ // container and building without the appenginevm build constraint.
+ // Check metadata.OnGCE instead.
+ if !metadata.OnGCE() {
+ return nil, fmt.Errorf("not running on appengine")
+ }
+ projID, err := metadata.ProjectID()
+ if err != nil {
+ return nil, err
+ }
+ return (*MonitoredResource)(&mrpb.MonitoredResource{
+ Type: "gae_instance",
+ Labels: map[string]string{
+ "project_id": projID,
+ "module_id": appengine.ModuleName(ctx),
+ "version_id": appengine.VersionID(ctx),
+ "instance_id": appengine.InstanceID(),
+ "location": appengine.Datacenter(ctx),
+ },
+ }), 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 errors.As(err, new(metadata.NotDefinedError)) {
+ return "", nil
+ } else if err != nil {
+ return "", err
+ }
+ // "created-by" format: "projects/{{InstanceID}}/zones/{{Zone}}/instanceGroupManagers/{{Instance Group Name}}
+ ig = path.Base(ig)
+ return ig, nil
+}
diff --git a/main.go b/main.go
index 94a96bfb..3adf653e 100644
--- a/main.go
+++ b/main.go
@@ -6,17 +6,25 @@ package main
import (
"context"
+ "flag"
"fmt"
"net/http"
"os"
"cloud.google.com/go/compute/metadata"
"cloud.google.com/go/datastore"
+ "golang.org/x/playground/internal/metrics"
)
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 == "" {
@@ -32,28 +40,67 @@ 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
+ if gotip := os.Getenv("GOTIP"); gotip == "true" {
+ s.gotip = true
+ }
+ execpath, _ := os.Executable()
+ if execpath != "" {
+ if fi, _ := os.Stat(execpath); fi != nil {
+ s.modtime = fi.ModTime()
+ }
+ }
+ eh, err := newExamplesHandler(s.gotip, s.modtime)
+ if err != nil {
+ return err
+ }
+ s.examples = eh
return nil
- })
+ }, enableMetrics)
if err != nil {
log.Fatalf("Error creating server: %v", err)
}
- if len(os.Args) > 1 && os.Args[1] == "test" {
+ if *runtests {
s.test()
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")
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))
}
+func enableMetrics(s *server) error {
+ gr, err := metrics.GAEResource(context.Background())
+ if err != nil {
+ s.log.Printf("metrics.GAEResource() = _, %q", err)
+ }
+ ms, err := metrics.NewService(gr, views)
+ if err != nil {
+ s.log.Printf("Failed to initialize metrics: metrics.NewService() = _, %q. (not on GCP?)", err)
+ }
+ if ms != nil && !metadata.OnGCE() {
+ s.mux.Handle("/metrics", ms)
+ }
+ return nil
+}
+
func projectID() string {
id, err := metadata.ProjectID()
if err != nil && os.Getenv("GAE_INSTANCE") != "" {
diff --git a/metrics.go b/metrics.go
new file mode 100644
index 00000000..895efc1b
--- /dev/null
+++ b/metrics.go
@@ -0,0 +1,72 @@
+// Copyright 2021 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.opencensus.io/stats"
+ "go.opencensus.io/stats/view"
+ "go.opencensus.io/tag"
+)
+
+var (
+ BuildLatencyDistribution = view.Distribution(1, 5, 10, 15, 20, 25, 50, 75, 100, 125, 150, 200, 250, 300, 400, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 7000, 8000, 9000, 10000, 20000, 30000)
+ kGoBuildSuccess = tag.MustNewKey("go-playground/frontend/go_build_success")
+ kGoRunSuccess = tag.MustNewKey("go-playground/frontend/go_run_success")
+ kGoVetSuccess = tag.MustNewKey("go-playground/frontend/go_vet_success")
+ mGoBuildLatency = stats.Float64("go-playground/frontend/go_build_latency", "", stats.UnitMilliseconds)
+ mGoRunLatency = stats.Float64("go-playground/frontend/go_run_latency", "", stats.UnitMilliseconds)
+ mGoVetLatency = stats.Float64("go-playground/frontend/go_vet_latency", "", stats.UnitMilliseconds)
+
+ goBuildCount = &view.View{
+ Name: "go-playground/frontend/go_build_count",
+ Description: "Number of snippets built",
+ Measure: mGoBuildLatency,
+ TagKeys: []tag.Key{kGoBuildSuccess},
+ Aggregation: view.Count(),
+ }
+ goBuildLatency = &view.View{
+ Name: "go-playground/frontend/go_build_latency",
+ Description: "Latency distribution of building snippets",
+ Measure: mGoBuildLatency,
+ Aggregation: BuildLatencyDistribution,
+ }
+ goRunCount = &view.View{
+ Name: "go-playground/frontend/go_run_count",
+ Description: "Number of snippets run",
+ Measure: mGoRunLatency,
+ TagKeys: []tag.Key{kGoRunSuccess},
+ Aggregation: view.Count(),
+ }
+ goRunLatency = &view.View{
+ Name: "go-playground/frontend/go_run_latency",
+ Description: "Latency distribution of running snippets",
+ Measure: mGoRunLatency,
+ Aggregation: BuildLatencyDistribution,
+ }
+ goVetCount = &view.View{
+ Name: "go-playground/frontend/go_vet_count",
+ Description: "Number of vet runs",
+ Measure: mGoVetLatency,
+ TagKeys: []tag.Key{kGoVetSuccess},
+ Aggregation: view.Count(),
+ }
+ goVetLatency = &view.View{
+ Name: "go-playground/sandbox/go_vet_latency",
+ Description: "Latency distribution of vet runs",
+ Measure: mGoVetLatency,
+ Aggregation: BuildLatencyDistribution,
+ }
+)
+
+// views should contain all measurements. All *view.View added to this
+// slice will be registered and exported to the metric service.
+var views = []*view.View{
+ goBuildCount,
+ goBuildLatency,
+ goRunCount,
+ goRunLatency,
+ goVetCount,
+ goVetLatency,
+}
diff --git a/play.go b/play.go
index e160220d..81e29df2 100644
--- a/play.go
+++ b/play.go
@@ -30,10 +30,10 @@ var epoch = time.Unix(1257894000, 0)
// occurring at the same time as the preceding event.
//
// A playback header has this structure:
-// 4 bytes: "\x00\x00PB", a magic header
-// 8 bytes: big-endian int64, unix time in nanoseconds
-// 4 bytes: big-endian int32, length of the next write
//
+// 4 bytes: "\x00\x00PB", a magic header
+// 8 bytes: big-endian int64, unix time in nanoseconds
+// 4 bytes: big-endian int32, length of the next write
type Recorder struct {
stdout, stderr recorderWriter
}
@@ -154,7 +154,7 @@ func decode(kind string, output []byte) ([]event, error) {
if t.Before(last) {
// Force timestamps to be monotonic. (This could
// be an encoding error, which we ignore now but will
- // will likely be picked up when decoding the length.)
+ // likely be picked up when decoding the length.)
t = last
}
n := int(binary.BigEndian.Uint32(header[8:]))
diff --git a/sandbox.go b/sandbox.go
index 36d2f28f..6e499999 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
@@ -13,41 +12,77 @@ import (
"context"
"crypto/sha256"
"encoding/json"
+ "errors"
"fmt"
"go/ast"
"go/doc"
"go/parser"
"go/token"
"io"
- "io/ioutil"
- stdlog "log"
+ "net"
"net/http"
"os"
"os/exec"
"path/filepath"
- "reflect"
"runtime"
+ "strconv"
"strings"
- "text/template"
+ "sync"
"time"
+ "unicode"
+ "unicode/utf8"
+ "cloud.google.com/go/compute/metadata"
"github.com/bradfitz/gomemcache/memcache"
+ "go.opencensus.io/stats"
+ "go.opencensus.io/tag"
+ "golang.org/x/playground/internal"
+ "golang.org/x/playground/internal/gcpdial"
+ "golang.org/x/playground/sandbox/sandboxtypes"
)
const (
- maxRunTime = 2 * time.Second
+ // Time for 'go build' to download 3rd-party modules and compile.
+ maxBuildTime = 10 * time.Second
+ maxRunTime = 5 * time.Second
+
+ // progName is the implicit program name written to the temp
+ // dir and used in compiler and vet errors.
+ progName = "prog.go"
+ progTestName = "prog_test.go"
+)
- // progName is the program name in compiler errors
- progName = "prog.go"
+const (
+ goBuildTimeoutError = "timeout running go build"
+ runTimeoutError = "timeout running program"
)
+// 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",
+}
+
type request struct {
- Body string
+ Body string
+ WithVet bool // whether client supports vet response in a /compile request (Issue 31970)
}
type response struct {
- Errors string
- Events []Event
+ Errors string
+ Events []Event
+ 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 & passed. 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.
@@ -56,8 +91,9 @@ 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", "*")
if r.Method == "OPTIONS" {
// This is likely a pre-flight CORS request.
@@ -69,39 +105,63 @@ 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)
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 {
- if err != memcache.ErrCacheMiss {
+ if !errors.Is(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)
return
}
+ if strings.Contains(resp.Errors, goBuildTimeoutError) || strings.Contains(resp.Errors, runTimeoutError) {
+ // TODO(golang.org/issue/38576) - This should be an 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.writeJSONResponse(w, resp, http.StatusOK)
+ return
+ }
+ 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)
+ return
+ }
+ }
+ for _, el := range resp.Events {
+ if el.Kind != "stderr" {
+ continue
+ }
+ 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)
+ return
+ }
+ }
+ }
if err := s.cache.Set(key, resp); err != nil {
s.log.Errorf("cache.Set(%q, resp): %v", key, err)
}
}
- 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.writeJSONResponse(w, resp, http.StatusOK)
}
}
@@ -111,7 +171,40 @@ func cacheKey(prefix, body string) string {
return fmt.Sprintf("%s-%s-%x", prefix, runtime.Version(), h.Sum(nil))
}
-// isTestFunc tells whether fn has the type of a testing function.
+// experiments returns the experiments listed in // GOEXPERIMENT=xxx comments
+// at the top of src.
+func experiments(src string) []string {
+ var exp []string
+ for src != "" {
+ line := src
+ src = ""
+ if i := strings.Index(line, "\n"); i >= 0 {
+ line, src = line[:i], line[i+1:]
+ }
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+ if !strings.HasPrefix(line, "//") {
+ break
+ }
+ line = strings.TrimSpace(strings.TrimPrefix(line, "//"))
+ if !strings.HasPrefix(line, "GOEXPERIMENT") {
+ continue
+ }
+ line = strings.TrimSpace(strings.TrimPrefix(line, "GOEXPERIMENT"))
+ if !strings.HasPrefix(line, "=") {
+ continue
+ }
+ line = strings.TrimSpace(strings.TrimPrefix(line, "="))
+ if line != "" {
+ exp = append(exp, line)
+ }
+ }
+ return exp
+}
+
+// isTestFunc tells whether fn has the type of a testing, or fuzz function, or a TestMain func.
func isTestFunc(fn *ast.FuncDecl) bool {
if fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
fn.Type.Params.List == nil ||
@@ -123,21 +216,21 @@ func isTestFunc(fn *ast.FuncDecl) bool {
if !ok {
return false
}
- // We can't easily check that the type is *testing.T
+ // We can't easily check that the type is *testing.T or *testing.F
// because we don't know how testing has been imported,
- // but at least check that it's *T or *something.T.
- if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "T" {
+ // but at least check that it's *T (or *F) or *something.T (or *something.F).
+ if name, ok := ptr.X.(*ast.Ident); ok && (name.Name == "T" || name.Name == "F" || name.Name == "M") {
return true
}
- if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "T" {
+ if sel, ok := ptr.X.(*ast.SelectorExpr); ok && (sel.Sel.Name == "T" || sel.Sel.Name == "F" || sel.Sel.Name == "M") {
return true
}
return false
}
-// isTest tells whether name looks like a test (or benchmark, according to prefix).
+// isTest tells whether name looks like a test (or benchmark, or fuzz, 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
@@ -145,39 +238,30 @@ 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.
+// isTestProg returns source code that executes all valid tests and examples in src.
// If the main function is present or there are no tests or examples, it returns nil.
// getTestProg emulates the "go test" command as closely as possible.
// Benchmarks are not supported because of sandboxing.
-func getTestProg(src []byte) []byte {
+func isTestProg(src []byte) bool {
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
- }
-
- // importPos stores the position to inject the "testing" import declaration, if needed.
- importPos := fset.Position(f.Name.End()).Offset
-
- var testingImported bool
- for _, s := range f.Imports {
- if s.Path.Value == `"testing"` && s.Name == nil {
- testingImported = true
- break
- }
+ return false
}
// 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
+ return false
}
- var tests []string
+ var hasTest bool
+ var hasFuzz bool
for _, d := range f.Decls {
n, ok := d.(*ast.FuncDecl)
if !ok {
@@ -188,503 +272,394 @@ func getTestProg(src []byte) []byte {
case name == "main":
// main declared as a method will not obstruct creation of our main function.
if n.Recv == nil {
- return nil
+ return false
}
+ case name == "TestMain" && isTestFunc(n):
+ hasTest = true
case isTest(name, "Test") && isTestFunc(n):
- tests = append(tests, name)
- }
- }
-
- // Tests imply imported "testing" package in the code.
- // If there is no import, bail to let the compiler produce an error.
- if !testingImported && len(tests) > 0 {
- return nil
- }
-
- // We emulate "go test". An example with no "Output" comment is compiled,
- // but not executed. An example with no text after "Output:" is compiled,
- // executed, and expected to produce no output.
- var ex []*doc.Example
- // exNoOutput indicates whether an example with no output is found.
- // We need to compile the program containing such an example even if there are no
- // other tests or examples.
- exNoOutput := false
- for _, e := range doc.Examples(f) {
- if e.Output != "" || e.EmptyOutput {
- ex = append(ex, e)
+ hasTest = true
+ case isTest(name, "Fuzz") && isTestFunc(n):
+ hasFuzz = true
}
- if e.Output == "" && !e.EmptyOutput {
- exNoOutput = true
- }
- }
-
- if len(tests) == 0 && len(ex) == 0 && !exNoOutput {
- return nil
}
- if !testingImported && (len(ex) > 0 || exNoOutput) {
- // In case of the program with examples and no "testing" package imported,
- // add import after "package main" without modifying line numbers.
- importDecl := []byte(`;import "testing";`)
- src = bytes.Join([][]byte{src[:importPos], importDecl, src[importPos:]}, nil)
+ if hasTest || hasFuzz {
+ return true
}
- data := struct {
- Tests []string
- Examples []*doc.Example
- }{
- tests,
- ex,
- }
- code := new(bytes.Buffer)
- if err := testTmpl.Execute(code, data); err != nil {
- panic(err)
- }
- src = append(src, code.Bytes()...)
- return src
+ return len(doc.Examples(f)) > 0
}
-var testTmpl = template.Must(template.New("main").Parse(`
-func main() {
- matchAll := func(t string, pat string) (bool, error) { return true, nil }
- tests := []testing.InternalTest{
-{{range .Tests}}
- {"{{.}}", {{.}}},
-{{end}}
- }
- examples := []testing.InternalExample{
-{{range .Examples}}
- {"Example{{.Name}}", Example{{.Name}}, {{printf "%q" .Output}}, {{.Unordered}}},
-{{end}}
- }
- testing.Main(matchAll, tests, nil, examples)
-}
-`))
+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,
// *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")
+ tmpDir, err := os.MkdirTemp("", "sandbox")
if err != nil {
return nil, fmt.Errorf("error creating temp directory: %v", err)
}
defer os.RemoveAll(tmpDir)
- src := []byte(req.Body)
- in := filepath.Join(tmpDir, "main.go")
- if err := ioutil.WriteFile(in, src, 0400); err != nil {
- return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
+ br, err := sandboxBuild(ctx, tmpDir, []byte(req.Body), req.WithVet)
+ if err != nil {
+ return nil, err
}
-
- fset := token.NewFileSet()
-
- 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
+ if br.errorMessage != "" {
+ return &response{Errors: removeBanner(br.errorMessage)}, nil
}
- var testParam string
- if code := getTestProg(src); code != nil {
- testParam = "-test.v"
- if err := ioutil.WriteFile(in, code, 0400); err != nil {
- return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
- }
+ execRes, err := sandboxRun(ctx, br.exePath, br.testParam)
+ if err != nil {
+ return nil, err
}
-
- 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")}
- 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'.
- errs := strings.Replace(string(out), in, progName, -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)
-
- return &response{Errors: errs}, nil
- }
- return nil, fmt.Errorf("error building go source: %v", err)
+ if execRes.Error != "" {
+ return &response{Errors: execRes.Error}, nil
}
- 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)
- cmd.Stdout = rec.Stdout()
- cmd.Stderr = rec.Stderr()
- if err := cmd.Run(); err != nil {
- if ctx.Err() == context.DeadlineExceeded {
- return &response{Errors: "process took too long"}, nil
- }
- if _, ok := err.(*exec.ExitError); !ok {
- return nil, fmt.Errorf("error running sandbox: %v", err)
- }
- }
+ 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)
}
- return &response{Events: events}, nil
-}
-
-func (s *server) healthCheck() error {
- resp, err := compileAndRun(&request{Body: healthProg})
- if err != nil {
- return err
- }
- if resp.Errors != "" {
- return fmt.Errorf("compile error: %v", resp.Errors)
+ 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)
+ }
}
- if len(resp.Events) != 1 || resp.Events[0].Message != "ok" {
- return fmt.Errorf("unexpected output: %v", resp.Events)
+ 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
+ // 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.goPath != "" {
+ return os.RemoveAll(b.goPath)
}
return nil
}
-const healthProg = `
-package main
+// 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) (br *buildResult, 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(kGoBuildSuccess, status)},
+ mGoBuildLatency.M(float64(time.Since(start))/float64(time.Millisecond)))
+ }()
-import "fmt"
+ files, err := splitFiles(in)
+ if err != nil {
+ return &buildResult{errorMessage: err.Error()}, nil
+ }
-func main() { fmt.Print("ok") }
-`
+ br = new(buildResult)
+ defer br.cleanup()
+ var buildPkgArg = "."
+ if len(files.Data(progName)) > 0 {
+ src := files.Data(progName)
+ if isTestProg(src) {
+ br.testParam = "-test.v"
+ files.MvFile(progName, progTestName)
+ }
+ }
-func (s *server) test() {
- if err := s.healthCheck(); err != nil {
- stdlog.Fatal(err)
+ if !files.Contains("go.mod") {
+ files.AddFile("go.mod", []byte("module play\n"))
}
- 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)
+
+ var exp []string
+ 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 &buildResult{errorMessage: "package name must be main"}, nil
}
- continue
+ exp = append(exp, experiments(string(src))...)
}
- if t.errors != "" {
- if resp.Errors != t.errors {
- stdlog.Fatalf("resp.Errors = %q, want %q", resp.Errors, t.errors)
+
+ in := filepath.Join(tmpDir, f)
+ if strings.Contains(f, "/") {
+ if err := os.MkdirAll(filepath.Dir(in), 0755); err != nil {
+ return nil, err
}
- 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)
+ if err := os.WriteFile(in, src, 0644); err != nil {
+ return nil, fmt.Errorf("error creating temp file %q: %v", in, err)
}
}
- 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")
+ br.exePath = filepath.Join(tmpDir, "a.out")
+ goCache := filepath.Join(tmpDir, "gocache")
+
+ // Copy the gocache directory containing .a files for std, so that we can
+ // avoid recompiling std during this build. Using -al (hard linking) is
+ // faster than actually copying the bytes.
+ //
+ // This is necessary as .a files are no longer included in GOROOT following
+ // https://go.dev/cl/432535.
+ if err := exec.Command("cp", "-al", "/gocache", goCache).Run(); err != nil {
+ return nil, fmt.Errorf("error copying GOCACHE: %v", err)
+ }
+
+ var goArgs []string
+ if br.testParam != "" {
+ goArgs = append(goArgs, "test", "-c")
+ } else {
+ goArgs = append(goArgs, "build")
+ }
+ goArgs = append(goArgs, "-o", br.exePath, "-tags=faketime")
+
+ cmd := exec.Command("/usr/local/go-faketime/bin/go", goArgs...)
+ cmd.Dir = tmpDir
+ cmd.Env = []string{"GOOS=linux", "GOARCH=amd64", "GOROOT=/usr/local/go-faketime"}
+ cmd.Env = append(cmd.Env, "GOCACHE="+goCache)
+ cmd.Env = append(cmd.Env, "CGO_ENABLED=0")
+ cmd.Env = append(cmd.Env, "GOEXPERIMENT="+strings.Join(exp, ","))
+ // Create a GOPATH just for modules to be downloaded
+ // into GOPATH/pkg/mod.
+ cmd.Args = append(cmd.Args, "-modcacherw")
+ cmd.Args = append(cmd.Args, "-mod=mod")
+ br.goPath, err = os.MkdirTemp("", "gopath")
if err != nil {
- panic(err.Error())
+ log.Printf("error creating temp directory: %v", err)
+ return nil, fmt.Errorf("error creating temp directory: %v", err)
}
- println(loc.String())
-}
-`, want: "America/New_York"},
+ cmd.Env = append(cmd.Env, "GO111MODULE=on", "GOPROXY="+playgroundGoproxy())
+ cmd.Args = append(cmd.Args, buildPkgArg)
+ cmd.Env = append(cmd.Env, "GOPATH="+br.goPath)
+ out := &bytes.Buffer{}
+ cmd.Stderr, cmd.Stdout = out, out
- {prog: `
-package main
-
-import (
- "fmt"
- "time"
-)
-
-func main() {
- fmt.Println(time.Now())
-}
-`, want: "2009-11-10 23:00:00 +0000 UTC"},
-
- {prog: `
-package main
+ if err := cmd.Start(); err != nil {
+ return nil, fmt.Errorf("error starting go build: %v", err)
+ }
+ ctx, cancel := context.WithTimeout(ctx, maxBuildTime)
+ 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)
-import (
- "fmt"
- "time"
-)
+ // "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)
-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
+ 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 {
+ if err != nil {
+ return nil, fmt.Errorf("failed to stat binary: %v", err)
}
- got = append(got, c)
+ return nil, fmt.Errorf("invalid binary size %d", fi.Size())
}
-}
-`, 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...")
+ if vet {
+ // TODO: do this concurrently with the execution to reduce latency.
+ br.vetOut, err = vetCheckInDir(ctx, tmpDir, br.goPath, exp)
+ if err != nil {
+ return nil, fmt.Errorf("running vet: %v", err)
+ }
}
+ return br, nil
}
-`, 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
+// sandboxRun runs a Go binary in a sandbox environment.
+func sandboxRun(ctx context.Context, exePath, testParam string) (execRes sandboxtypes.Response, 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(kGoBuildSuccess, status)},
+ mGoRunLatency.M(float64(time.Since(start))/float64(time.Millisecond)))
+ }()
+ exeBytes, err := os.ReadFile(exePath)
+ if err != nil {
+ return execRes, err
+ }
+ ctx, cancel := context.WithTimeout(ctx, maxRunTime)
+ defer cancel()
+ sreq, err := http.NewRequestWithContext(ctx, "POST", sandboxBackendURL(), bytes.NewReader(exeBytes))
+ if err != nil {
+ return execRes, 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 io.NopCloser(bytes.NewReader(exeBytes)), nil }
+ res, err := sandboxBackendClient().Do(sreq)
+ if err != nil {
+ if errors.Is(ctx.Err(), context.DeadlineExceeded) {
+ execRes.Error = runTimeoutError
+ return execRes, nil
+ }
+ 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 execRes, fmt.Errorf("unexpected response from backend: %v", res.Status)
+ }
+ if err := json.NewDecoder(res.Body).Decode(&execRes); err != nil {
+ log.Printf("JSON decode error from backend: %v", err)
+ return execRes, errors.New("error parsing JSON from backend")
+ }
+ return execRes, nil
}
-`, 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
+// 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"
}
-`, want: `=== RUN ExampleUnorderedOutput
---- PASS: ExampleUnorderedOutput (0.00s)
-PASS`},
-
- {prog: `
-package main
-import "fmt"
-
-func ExampleEmptyOutput() {
- // Output:
+// 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 := os.MkdirTemp("", "sandbox")
+ if err != nil {
+ return fmt.Errorf("error creating temp directory: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+ br, err := sandboxBuild(ctx, tmpDir, []byte(healthProg), false)
+ if err != nil {
+ return err
+ }
+ if br.errorMessage != "" {
+ return errors.New(br.errorMessage)
+ }
+ return nil
}
-func ExampleEmptyOutputFail() {
- fmt.Println("1")
- // Output:
+// 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
+ }
+ 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))
+}
+
+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
+ }
}
-`, 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)
+// removeBanner remove package name banner
+func removeBanner(output string) string {
+ if strings.HasPrefix(output, "#") {
+ if nl := strings.Index(output, "\n"); nl != -1 {
+ output = output[nl+1:]
+ }
+ }
+ return output
}
-`, want: `testing: warning: no tests to run
-PASS`},
- {prog: `
+const healthProg = `
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},
- }},
-}
+func main() { fmt.Print("ok") }
+`
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..2ab1fcbb
--- /dev/null
+++ b/sandbox/Dockerfile
@@ -0,0 +1,39 @@
+# 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.25-trixie 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
+
+FROM debian:trixie
+
+RUN apt-get update
+
+# Extra stuff for occasional debugging:
+RUN apt-get install --yes strace lsof emacs-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 trixie 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..6e3a7585
--- /dev/null
+++ b/sandbox/Dockerfile.gvisor
@@ -0,0 +1,19 @@
+# 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
+
+############################################################################
+# 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
+COPY --from=server /usr/share/zoneinfo /usr/share/zoneinfo
+
+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..66e4090f
--- /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/gvisor/releases/release/latest/x86_64/runsc
+- 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..1f424569
--- /dev/null
+++ b/sandbox/konlet.yaml
@@ -0,0 +1,16 @@
+spec:
+ containers:
+ - name: playground
+ image: 'gcr.io/PROJECT_NAME/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/metrics.go b/sandbox/metrics.go
new file mode 100644
index 00000000..849bcced
--- /dev/null
+++ b/sandbox/metrics.go
@@ -0,0 +1,119 @@
+// 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.opencensus.io/plugin/ochttp"
+ "go.opencensus.io/stats"
+ "go.opencensus.io/stats/view"
+ "go.opencensus.io/tag"
+)
+
+var (
+ 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",
+ 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(),
+ }
+ 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",
+ 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(),
+ }
+)
+
+// views should contain all measurements. All *view.View added to this
+// slice will be registered and exported to the metric service.
+var views = []*view.View{
+ containerCount,
+ unwantedContainerCount,
+ maxContainerCount,
+ containerCreateCount,
+ containerCreationLatency,
+ ServerRequestCountView,
+ ServerRequestBytesView,
+ ServerResponseBytesView,
+ ServerLatencyView,
+ ServerRequestCountByMethod,
+ ServerResponseCountByStatusCode,
+}
diff --git a/sandbox/sandbox.go b/sandbox/sandbox.go
new file mode 100644
index 00000000..29c4671c
--- /dev/null
+++ b/sandbox/sandbox.go
@@ -0,0 +1,733 @@
+// 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 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
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "os/exec"
+ "os/signal"
+ "runtime"
+ "sync"
+ "syscall"
+ "time"
+
+ "cloud.google.com/go/compute/metadata"
+ "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/internal/metrics"
+ "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")
+ container = flag.String("untrusted-container", "gcr.io/golang-org/playground-sandbox-gvisor:latest", "container image name that hosts the untrusted binary under gvisor")
+)
+
+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")
+ 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
+// server know that a particular container is ready to run a binary.
+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
+ runSem chan struct{}
+)
+
+type Container struct {
+ name string
+
+ stdin io.WriteCloser
+ stdout *limitedWriter
+ stderr *limitedWriter
+
+ cmd *exec.Cmd
+ cancelCmd context.CancelFunc
+
+ waitErr chan error // 1-buffered; receives error from WaitOrStop(..., cmd, ...)
+}
+
+func (c *Container) Close() {
+ setContainerWanted(c.name, false)
+
+ 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 {
+ err := <-c.waitErr
+ c.waitErr <- err
+ return err
+}
+
+var httpServer *http.Server
+
+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)
+ runSem = make(chan struct{}, *numWorkers)
+ go handleSignals()
+
+ mux := http.NewServeMux()
+
+ gr, err := metrics.GCEResource("go-playground-sandbox")
+ if err != nil && metadata.OnGCE() {
+ log.Printf("metrics.GceService(%q) = _, %v, wanted no error.", "go-playground-sandbox", err)
+ }
+ if ms, err := metrics.NewService(gr, views); err != nil {
+ log.Printf("Failed to initialize metrics: metrics.NewService() = _, %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)
+ }
+ if *dev {
+ log.Printf("Running in dev mode; container published to host at: http://localhost:8080/")
+ 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)
+ }
+
+ 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"))
+
+ 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{
+ Addr: *listenAddr,
+ Handler: &ochttp.Handler{Handler: mux},
+ }
+ 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 its 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)
+ s := <-c
+ 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")
+}
+
+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 := io.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 := os.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 {
+ log.Fatalf("error decoding JSON meta: %v", err)
+ }
+
+ if _, err := os.Stderr.Write(containedStderrHeader); err != nil {
+ log.Fatalf("writing header to stderr: %v", err)
+ }
+
+ cmd := exec.Command(binPath)
+ cmd.Args = append(cmd.Args, meta.Args...)
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ if err := cmd.Start(); err != nil {
+ log.Fatalf("cmd.Start(): %v", err)
+ }
+ 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
+}
+
+func makeWorkers() {
+ ctx := context.Background()
+ stats.Record(ctx, mMaxContainers.M(int64(*numWorkers)))
+ for i := 0; i < *numWorkers; i++ {
+ go workerLoop(ctx)
+ }
+}
+
+func workerLoop(ctx context.Context) {
+ for {
+ c, err := startContainer(ctx)
+ 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 isContainerWanted(name string) bool {
+ wantedMu.Lock()
+ defer wantedMu.Unlock()
+ return 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) {
+ 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",
+ "--name="+name,
+ "--rm",
+ "--tmpfs=/tmpfs:exec",
+ "-i", // read stdin
+
+ "--runtime=runsc",
+ "--network=none",
+ "--memory="+fmt.Sprint(memoryLimitBytes),
+
+ *container,
+ "--mode=contained")
+ 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
+ }
+
+ 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 {
+ c.Close()
+ }
+ }()
+
+ startErr := make(chan error, 1)
+ go func() {
+ buf := make([]byte, len(containedStartMessage))
+ _, 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
+ }
+ }()
+
+ timer := time.NewTimer(startTimeout)
+ defer timer.Stop()
+ select {
+ case <-timer.C:
+ err := fmt.Errorf("timeout starting container %q", name)
+ cancel()
+ <-startErr
+ return nil, err
+
+ case err := <-startErr:
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ log.Printf("started container %q", name)
+ return c, 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 := io.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
+ }
+ 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)
+
+ ctx, cancel := context.WithTimeout(context.Background(), runTimeout)
+ closed := make(chan struct{})
+ defer func() {
+ logf("leaving handler; about to close container")
+ cancel()
+ <-closed
+ }()
+ go func() {
+ <-ctx.Done()
+ if ctx.Err() == context.DeadlineExceeded {
+ logf("timeout")
+ }
+ c.Close()
+ close(closed)
+ }()
+ 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 <-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
+ }
+ var ee *exec.ExitError
+ if !errors.As(err, &ee) {
+ http.Error(w, "unknown error during docker run", http.StatusInternalServerError)
+ return
+ }
+ res.ExitCode = ee.ExitCode()
+ }
+ 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
+ }
+ var ee *exec.ExitError
+ if errors.As(err, &ee) {
+ 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 {
+ i := bytes.Index(x, containedStderrHeader)
+ if i == -1 {
+ return x
+ }
+ return x[i+len(containedStderrHeader):]
+}
diff --git a/sandbox/sandbox_test.go b/sandbox/sandbox_test.go
new file mode 100644
index 00000000..fac52bb2
--- /dev/null
+++ b/sandbox/sandbox_test.go
@@ -0,0 +1,226 @@
+// 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 (
+ "bytes"
+ "io"
+ "strings"
+ "testing"
+ "testing/iotest"
+
+ "github.com/google/go-cmp/cmp"
+)
+
+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")
+ }
+}
+
+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)
+ }
+ })
+ }
+}
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/sandbox_test.go b/sandbox_test.go
new file mode 100644
index 00000000..0beae040
--- /dev/null
+++ b/sandbox_test.go
@@ -0,0 +1,109 @@
+// 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 (
+ "os/exec"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+// TestExperiments tests that experiment lines are recognized.
+func TestExperiments(t *testing.T) {
+ var tests = []struct {
+ src string
+ exp []string
+ }{
+ {"//GOEXPERIMENT=active\n\npackage main", []string{"active"}},
+ {" // GOEXPERIMENT= active \n\npackage main", []string{"active"}},
+ {" // GOEXPERIMENT= active \n\npackage main", []string{"active"}},
+ {" // GOEXPERIMENT = active \n\npackage main", []string{"active"}},
+ {"//GOEXPERIMENT=foo\n\n// GOEXPERIMENT=bar\n\npackage main", []string{"foo", "bar"}},
+ {"/* hello world */\n// GOEXPERIMENT=ignored\n", nil},
+ {"package main\n// GOEXPERIMENT=ignored\n", nil},
+ }
+
+ for _, tt := range tests {
+ if exp := experiments(tt.src); !reflect.DeepEqual(exp, tt.exp) {
+ t.Errorf("experiments(%q) = %q, want %q", tt.src, exp, tt.exp)
+ }
+ }
+}
+
+// TestIsTest verifies that the isTest helper function matches
+// exactly (and only) the names of functions recognized as tests.
+func TestIsTest(t *testing.T) {
+ // We must disable vet's "tests" analyzer which would otherwise cause
+ // go test to fail due to the intentional problems in testdata/p's tests.
+ cmd := exec.Command("go", "test", "./testdata/p", "-vet=off", "-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 {
+ // We want Test/Benchmark/Example/Fuzz functions.
+ // Reject extraneous output such as "ok ...".
+ if line == "" || !strings.Contains("TBEF", line[:1]) {
+ continue
+ }
+ isTestFunction[strings.TrimSpace(line)] = true
+ }
+
+ for _, tc := range []struct {
+ prefix string
+ name string // name of a Test (etc) in ./testdata/p
+ want bool
+ }{
+ {"Test", "Test", 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},
+
+ {"Fuzz", "Fuzz", true},
+ {"Fuzz", "Fuzz1IsAFuzz", true},
+ {"Fuzz", "FuzzÑIsAFuzz", true},
+
+ {"Fuzz", "FuzzisNotAFuzz", false},
+ } {
+ name := tc.name
+ 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)
+ }
+ })
+ }
+}
diff --git a/server.go b/server.go
index d3a491b1..1ecb1343 100644
--- a/server.go
+++ b/server.go
@@ -5,9 +5,11 @@
package main
import (
+ "bytes"
+ "encoding/json"
"fmt"
+ "io"
"net/http"
- "os"
"strings"
"time"
@@ -15,10 +17,12 @@ import (
)
type server struct {
- mux *http.ServeMux
- db store
- log logger
- cache *gobCache
+ mux *http.ServeMux
+ db store
+ log logger
+ cache responseCache
+ gotip bool // if set, server is using gotip
+ examples *examplesHandler
// When the executable was last modified. Used for caching headers of compiled assets.
modtime time.Time
@@ -37,11 +41,8 @@ func newServer(options ...func(s *server) error) (*server, error) {
if s.log == nil {
return nil, fmt.Errorf("must provide an option func that specifies a logger")
}
- execpath, _ := os.Executable()
- if execpath != "" {
- if fi, _ := os.Stat(execpath); fi != nil {
- s.modtime = fi.ModTime()
- }
+ if s.examples == nil {
+ return nil, fmt.Errorf("must provide an option func that sets the examples handler")
}
s.init()
return s, nil
@@ -49,7 +50,8 @@ func newServer(options ...func(s *server) error) (*server, error) {
func (s *server) init() {
s.mux.HandleFunc("/", s.handleEdit)
- s.mux.HandleFunc("/fmt", handleFmt)
+ s.mux.HandleFunc("/fmt", s.handleFmt)
+ s.mux.HandleFunc("/version", s.handleVersion)
s.mux.HandleFunc("/vet", s.commandHandler("vet", vetCheck))
s.mux.HandleFunc("/compile", s.commandHandler("prog", compileAndRun))
s.mux.HandleFunc("/share", s.handleShare)
@@ -59,6 +61,7 @@ func (s *server) init() {
staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))
s.mux.Handle("/static/", staticHandler)
+ s.mux.Handle("/doc/play/", http.StripPrefix("/doc/play/", s.examples))
}
func (s *server) handlePlaygroundJS(w http.ResponseWriter, r *http.Request) {
@@ -72,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
}
@@ -91,3 +94,20 @@ func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
s.mux.ServeHTTP(w, r)
}
+
+// writeJSONResponse JSON-encodes resp and writes to w with the given HTTP
+// status.
+func (s *server) writeJSONResponse(w http.ResponseWriter, resp interface{}, status int) {
+ w.Header().Set("Content-Type", "application/json")
+ 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
+ }
+}
diff --git a/server_test.go b/server_test.go
index 0c174075..b1a73a30 100644
--- a/server_test.go
+++ b/server_test.go
@@ -1,15 +1,25 @@
// 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 (
"bytes"
+ "context"
+ "encoding/json"
"fmt"
- "io/ioutil"
+ "io"
"net/http"
"net/http/httptest"
+ "os"
+ "runtime"
+ "sync"
"testing"
+ "time"
+
+ "github.com/bradfitz/gomemcache/memcache"
+ "github.com/google/go-cmp/cmp"
)
type testLogger struct {
@@ -30,6 +40,11 @@ func testingOptions(t *testing.T) func(s *server) error {
return func(s *server) error {
s.db = &inMemStore{}
s.log = testLogger{t}
+ var err error
+ s.examples, err = newExamplesHandler(false, time.Now())
+ if err != nil {
+ return err
+ }
return nil
}
}
@@ -42,29 +57,36 @@ 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 {
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},
- {"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.StatusFound, 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)
}
@@ -75,9 +97,9 @@ func TestEdit(t *testing.T) {
}
if tc.respBody != nil {
defer resp.Body.Close()
- b, err := ioutil.ReadAll(resp.Body)
+ b, err := io.ReadAll(resp.Body)
if err != nil {
- t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err)
+ t.Errorf("%s: io.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)
@@ -86,42 +108,56 @@ func TestEdit(t *testing.T) {
}
}
-func TestShare(t *testing.T) {
+func TestServer(t *testing.T) {
s, err := newServer(testingOptions(t))
if err != nil {
t.Fatalf("newServer(testingOptions(t)): %v", err)
}
- const url = "https://play.golang.org/share"
+ const shareURL = "https://play.golang.org/share"
testCases := []struct {
desc string
method string
+ url string
statusCode int
reqBody []byte
respBody []byte
}{
- {"OPTIONS no-op", http.MethodOptions, http.StatusOK, nil, nil},
- {"Non-POST request", http.MethodGet, http.StatusMethodNotAllowed, nil, nil},
- {"Standard flow", http.MethodPost, http.StatusOK, []byte("Snippy McSnipface"), []byte("N_M_YelfGeR")},
- {"Snippet too large", http.MethodPost, http.StatusRequestEntityTooLarge, make([]byte, maxSnippetSize+1), nil},
+ // Share tests.
+ {"OPTIONS no-op", http.MethodOptions, shareURL, http.StatusOK, nil, nil},
+ {"Non-POST request", http.MethodGet, shareURL, http.StatusMethodNotAllowed, nil, nil},
+ {"Standard flow", http.MethodPost, shareURL, http.StatusOK, []byte("Snippy McSnipface"), []byte("N_M_YelfGeR")},
+ {"Snippet too large", http.MethodPost, shareURL, http.StatusRequestEntityTooLarge, make([]byte, maxSnippetSize+1), nil},
+
+ // Examples tests.
+ {"Hello example", http.MethodGet, "https://play.golang.org/doc/play/hello.txt", http.StatusOK, nil, []byte("Hello")},
+ {"HTTP example", http.MethodGet, "https://play.golang.org/doc/play/http.txt", http.StatusOK, nil, []byte("net/http")},
+ // Gotip examples should not be available on the non-tip playground.
+ {"Gotip example", http.MethodGet, "https://play.golang.org/doc/play/min.gotip.txt", http.StatusNotFound, nil, nil},
+
+ {"Versions json", http.MethodGet, "https://play.golang.org/version", http.StatusOK, nil, []byte(runtime.Version())},
}
for _, tc := range testCases {
- req := httptest.NewRequest(tc.method, url, bytes.NewReader(tc.reqBody))
+ req := httptest.NewRequest(tc.method, tc.url, bytes.NewReader(tc.reqBody))
w := httptest.NewRecorder()
- s.handleShare(w, req)
+ s.mux.ServeHTTP(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)
+ b, err := io.ReadAll(resp.Body)
if err != nil {
- t.Errorf("%s: ioutil.ReadAll(resp.Body): %v", tc.desc, err)
+ t.Errorf("%s: io.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)
+ if !bytes.Contains(b, tc.respBody) {
+ t.Errorf("%s: got unexpected body %q; want contains %q", tc.desc, b, tc.respBody)
}
}
}
@@ -152,66 +188,200 @@ 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)
+ var err error
+ s.examples, err = newExamplesHandler(false, time.Now())
+ if err != nil {
+ return err
+ }
return nil
})
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")
}
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
+ }
+ 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
+ }
+ 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}]}
+ []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}
+ []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, false},
+ {"Cannot allocate memory error in response body event message", http.MethodPost, http.StatusInternalServerError,
+ []byte(`{"Body":"allocate-memory-error"}`), nil, false},
+ {"Out of memory error in response errors", http.MethodPost, http.StatusInternalServerError,
+ []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, 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 := io.ReadAll(resp.Body)
+ if err != nil {
+ t.Errorf("%s: io.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)
+ }
+ })
+ }
+}
+
+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)
+ }
+ })
+ }
+}
+
+// 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
}
diff --git a/share.go b/share.go
index 6dd5adc1..528f86d1 100644
--- a/share.go
+++ b/share.go
@@ -52,11 +52,6 @@ func (s *server) handleShare(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Requires POST", http.StatusMethodNotAllowed)
return
}
- if !allowShare(r) {
- http.Error(w, "Either this isn't available in your country due to legal reasons, or our IP geolocation is wrong.",
- http.StatusUnavailableForLegalReasons)
- return
- }
var body bytes.Buffer
_, err := io.Copy(&body, io.LimitReader(r.Body, maxSnippetSize+1))
@@ -81,10 +76,3 @@ func (s *server) handleShare(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, id)
}
-
-func allowShare(r *http.Request) bool {
- if r.Header.Get("X-AppEngine-Country") == "CN" {
- return false
- }
- return true
-}
diff --git a/static/style.css b/static/style.css
index 1cd00e81..e9d2c253 100644
--- a/static/style.css
+++ b/static/style.css
@@ -79,6 +79,9 @@ a {
margin: 0;
}
#banner {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
position: absolute;
left: 0;
right: 0;
@@ -86,24 +89,24 @@ a {
height: 50px;
background-color: #E0EBF5;
}
+#banner > * {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ margin-right: 5px;
+ border-radius: 5px;
+ box-sizing: border-box;
+ height: 30px;
+}
#head {
- float: left;
- padding: 15px 10px;
-
+ padding-left: 10px;
+ padding-right: 20px;
+ padding-top: 5px;
font-size: 20px;
font-family: sans-serif;
}
-#controls {
- float: left;
- padding: 10px 15px;
- min-width: 245px;
-}
-#controls > input {
- border-radius: 5px;
-}
-#aboutControls {
- float: right;
- padding: 10px 15px;
+#aboutButton {
+ margin-left: auto;
+ margin-right: 15px;
}
input[type=button],
#importsBox {
@@ -116,19 +119,21 @@ input[type=button],
position: static;
top: 1px;
border-radius: 5px;
+ -webkit-appearance: none;
}
#importsBox {
- position: relative;
- display: inline;
- padding: 5px 0;
- margin-right: 5px;
+ padding: 0.25em 7px;
}
#importsBox input {
- position: relative;
- top: -2px;
- left: 1px;
- height: 10px;
- width: 10px;
+ flex: none;
+ height: 11px;
+ width: 11px;
+ margin: 0 5px 0 0;
+}
+#importsBox label {
+ display: flex;
+ align-items: center;
+ line-height: 1.2;
}
#shareURL {
width: 280px;
@@ -136,10 +141,14 @@ input[type=button],
border: 1px solid #ccc;
background: #eee;
color: black;
- height: 23px;
}
#embedLabel {
font-family: sans-serif;
+ padding-top: 5px;
+}
+#banner > select {
+ font-size: 0.875rem;
+ border: 0.0625rem solid #375EAB;
}
.lines {
float: left;
diff --git a/strict-time.patch b/strict-time.patch
deleted file mode 100644
index 86e663d1..00000000
--- a/strict-time.patch
+++ /dev/null
@@ -1,83 +0,0 @@
-From 9fd794d3a2e2b02688fe3a42c4c2c6d19d8dd668 Mon Sep 17 00:00:00 2001
-From: "Bryan C. Mills"