From 5f5f230b60c6b905712bf5d3e7555f43877c28f8 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Fri, 18 Jun 2021 23:19:15 -0400 Subject: [PATCH] all: promote new app deployments only after they become ready There are differences in the App Engine environment that cannot be adequately simulated elsewhere. Although we do the best we can with testing locally, there will always be differences. The old makefiles deployed the site, then ran a regression test against it, and then promoted the tested version. This change does the same. The testing has moved into the web server proper so that it can test the handler directly and thereby check things like the responses on different domains. The go-app-deploy.sh now always deploys --no-promote, only promoting after a self-test passes on the deployed site. Unlike the old check which only applied to golang.org, the new pre-promotion testing happens for all the sites. Also factor out GoogleCN into internal/web, because we needed to modify it (to avoid internal/webtest's requests being diagnosed as coming from China) and there were too many copies. Change-Id: I0cde0e2167df2332939908e716ddb6bf429f2565 Reviewed-on: https://go-review.googlesource.com/c/website/+/329250 Trust: Russ Cox Reviewed-by: Dmitri Shuralyov --- blog/blog.go | 5 ++ cmd/golangorg/app.yaml | 1 - cmd/golangorg/server.go | 25 ++------ cmd/golangorg/server_test.go | 80 ------------------------ cmd/golangorg/testdata/live.txt | 14 ++--- cmd/golangorg/testdata/release.txt | 2 +- cmd/golangorg/testdata/web.txt | 86 +++++++++++++------------- cmd/golangorg/testdata/x.txt | 12 ++-- go-app-deploy.sh | 66 +++++++++++++++++--- go.dev/cmd/frontend/main.go | 25 ++++++-- go.dev/cmd/frontend/server_test.go | 19 ++++++ go.dev/cmd/frontend/testdata/godev.txt | 5 ++ internal/dl/server.go | 20 ------ internal/env/env.go | 6 -- internal/proxy/proxy.go | 24 +------ internal/web/googlecn.go | 28 +++++++++ internal/web/site.go | 12 +--- internal/webtest/webtest.go | 20 ++++++ tour/appengine.go | 8 ++- tour/local.go | 6 +- tour/server_test.go | 24 +++++++ tour/testdata/tour.txt | 3 + 22 files changed, 259 insertions(+), 232 deletions(-) create mode 100644 go.dev/cmd/frontend/server_test.go create mode 100644 go.dev/cmd/frontend/testdata/godev.txt create mode 100644 internal/web/googlecn.go create mode 100644 tour/server_test.go create mode 100644 tour/testdata/tour.txt diff --git a/blog/blog.go b/blog/blog.go index 04c48513..5535e5a2 100644 --- a/blog/blog.go +++ b/blog/blog.go @@ -20,6 +20,7 @@ import ( _ "golang.org/x/tools/playground" "golang.org/x/website" "golang.org/x/website/internal/backport/httpfs" + "golang.org/x/website/internal/webtest" ) var ( @@ -66,6 +67,10 @@ func main() { if err != nil { log.Fatal(err) } + + h = webtest.HandlerWithCheck(h, "/_readycheck", + filepath.Join(blogRoot, "testdata/*.txt")) + http.Handle("/", h) ln, err := net.Listen("tcp", *httpAddr) diff --git a/cmd/golangorg/app.yaml b/cmd/golangorg/app.yaml index c03697de..9d6401b3 100644 --- a/cmd/golangorg/app.yaml +++ b/cmd/golangorg/app.yaml @@ -6,7 +6,6 @@ runtime: go115 main: ./cmd/golangorg env_variables: - GOLANGORG_CHECK_COUNTRY: true GOLANGORG_REQUIRE_DL_SECRET_KEY: true GOLANGORG_ENFORCE_HOSTS: true GOLANGORG_REDIS_ADDR: 10.0.0.4:6379 # instance "gophercache" diff --git a/cmd/golangorg/server.go b/cmd/golangorg/server.go index cedde620..32614431 100644 --- a/cmd/golangorg/server.go +++ b/cmd/golangorg/server.go @@ -36,6 +36,7 @@ import ( "golang.org/x/website/internal/redirect" "golang.org/x/website/internal/short" "golang.org/x/website/internal/web" + "golang.org/x/website/internal/webtest" // Registers "/compile" handler that redirects to play.golang.org/compile. // If we are in prod we will register "golang.org/compile" separately, @@ -93,6 +94,9 @@ func main() { handler := NewHandler(*contentDir, *goroot) + handler = webtest.HandlerWithCheck(handler, "/_readycheck", + filepath.Join(*contentDir, "../cmd/golangorg/testdata/*.txt")) + if *verbose { log.Printf("golang.org server:") log.Printf("\tversion = %s", runtime.Version()) @@ -137,7 +141,6 @@ func NewHandler(contentDir, goroot string) http.Handler { if err != nil { log.Fatalf("NewSite: %v", err) } - site.GoogleCN = googleCN mux := http.NewServeMux() mux.Handle("/", site) @@ -195,26 +198,6 @@ func appEngineSetup(site *web.Site, mux *http.ServeMux) { log.Println("AppEngine initialization complete") } -// googleCN reports whether request r is considered -// to be served from golang.google.cn. -// TODO: This is duplicated within internal/proxy. Move to a common location. -func googleCN(r *http.Request) bool { - if r.FormValue("googlecn") != "" { - return true - } - if strings.HasSuffix(r.Host, ".cn") { - return true - } - if !env.CheckCountry() { - return false - } - switch r.Header.Get("X-Appengine-Country") { - case "", "ZZ", "CN": - return true - } - return false -} - func blogHandler(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://blog.golang.org"+strings.TrimPrefix(r.URL.Path, "/blog"), http.StatusFound) } diff --git a/cmd/golangorg/server_test.go b/cmd/golangorg/server_test.go index 7d2b24fb..2421a05e 100644 --- a/cmd/golangorg/server_test.go +++ b/cmd/golangorg/server_test.go @@ -5,80 +5,13 @@ package main import ( - "bytes" - "flag" - "fmt" - "io/ioutil" - "net" - "net/http" - "os" - "os/exec" "path/filepath" "runtime" - "strings" "testing" - "time" "golang.org/x/website/internal/webtest" ) -func serverAddress(t *testing.T) string { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - ln, err = net.Listen("tcp6", "[::1]:0") - } - if err != nil { - t.Fatal(err) - } - defer ln.Close() - return ln.Addr().String() -} - -func waitForServerReady(t *testing.T, addr string) { - waitForServer(t, - fmt.Sprintf("http://%v/", addr), - "The Go Programming Language", - 15*time.Second) -} - -const pollInterval = 200 * time.Millisecond - -func waitForServer(t *testing.T, url, match string, timeout time.Duration) { - // "health check" duplicated from x/tools/cmd/tipgodoc/tip.go - deadline := time.Now().Add(timeout) - for time.Now().Before(deadline) { - time.Sleep(pollInterval) - res, err := http.Get(url) - if err != nil { - continue - } - rbody, err := ioutil.ReadAll(res.Body) - res.Body.Close() - if err == nil && res.StatusCode == http.StatusOK { - if bytes.Contains(rbody, []byte(match)) { - return - } - } - } - t.Fatalf("Server failed to respond in %v", timeout) -} - -func killAndWait(cmd *exec.Cmd) { - cmd.Process.Kill() - cmd.Wait() -} - -func init() { - // TestWeb reinvokes the test binary (us) with -be-main - // to simulate running the actual golangorg binary. - if len(os.Args) >= 2 && os.Args[1] == "-be-main" { - os.Args = os.Args[1:] - os.Args[0] = "(golangorg)" - main() - os.Exit(0) - } -} - func TestWeb(t *testing.T) { h := NewHandler("../../_content", runtime.GOROOT()) files, err := filepath.Glob("testdata/*.txt") @@ -91,16 +24,3 @@ func TestWeb(t *testing.T) { } } } - -// Regression tests to run against a production instance of golangorg. - -var host = flag.String("regtest.host", "", "host to run regression test against") - -func TestLiveServer(t *testing.T) { - *host = strings.TrimSuffix(*host, "/") - if *host == "" { - t.Skip("regtest.host flag missing.") - } - - webtest.TestServer(t, "testdata/*.txt", *host) -} diff --git a/cmd/golangorg/testdata/live.txt b/cmd/golangorg/testdata/live.txt index 92c5a294..f31a4c7a 100644 --- a/cmd/golangorg/testdata/live.txt +++ b/cmd/golangorg/testdata/live.txt @@ -1,36 +1,36 @@ # Tests that can only run against the live server, # because they depend on production resources. -GET /dl/ +GET https://golang.org/dl/ body contains href="/dl/go1.11.windows-amd64.msi" -GET /dl/?mode=json +GET https://golang.org/dl/?mode=json body contains .windows-amd64.msi body !contains UA- -GET /s/go2design +GET https://golang.org/s/go2design code == 302 body ~ proposal.*Found body !contains UA- -POST /compile +POST https://golang.org/compile postquery body=package main; func main() { print(6*7); } body == {"compile_errors":"","output":"42"} -POST /compile +POST https://golang.org/compile postquery body=//empty body contains expected 'package', found 'EOF' body !contains UA- -POST /compile +POST https://golang.org/compile postquery version=2 body=package main; import ("fmt"; "time"); func main() {fmt.Print("A"); time.Sleep(time.Second); fmt.Print("B")} body == {"Errors":"","Events":[{"Message":"A","Kind":"stdout","Delay":0},{"Message":"B","Kind":"stdout","Delay":1000000000}]} -POST /share +POST https://golang.org/share postbody package main body !contains UA- diff --git a/cmd/golangorg/testdata/release.txt b/cmd/golangorg/testdata/release.txt index 5724f4f7..fc38347b 100644 --- a/cmd/golangorg/testdata/release.txt +++ b/cmd/golangorg/testdata/release.txt @@ -1,4 +1,4 @@ -GET /doc/devel/release +GET https://golang.org/doc/devel/release header content-type == text/html; charset=utf-8 trimbody contains

go1.14 (released 2020-02-25)

diff --git a/cmd/golangorg/testdata/web.txt b/cmd/golangorg/testdata/web.txt index 3279d8eb..676e9d78 100644 --- a/cmd/golangorg/testdata/web.txt +++ b/cmd/golangorg/testdata/web.txt @@ -1,4 +1,4 @@ -GET / +GET https://golang.org/ body contains Go is an open source programming language body contains Binary distributions available for @@ -11,143 +11,143 @@ body contains Binary distributions available for body contains href="/golang.org/doc body !contains href="/doc -GET /change/75944e2e3a63 +GET https://golang.org/change/75944e2e3a63 code == 302 redirect contains bdb10cf body contains bdb10cf body !contains UA- -GET /cmd/compile/internal/amd64/ +GET https://golang.org/cmd/compile/internal/amd64/ body contains href="/src/cmd/compile/internal/amd64/ssa.go" -GET /conduct +GET https://golang.org/conduct body contains Project Stewards -GET /doc/ +GET https://golang.org/doc/ body contains an introduction to using modules in a simple project -GET /doc/asm +GET https://golang.org/doc/asm body ~ Quick Guide.*Assembler -GET /doc/debugging_with_gdb.html +GET https://golang.org/doc/debugging_with_gdb.html redirect == /doc/gdb -GET /doc/devel/release +GET https://golang.org/doc/devel/release body ~ go1\.14\.2\s+\(released 2020-04-08\)\s+includes\s+fixes to cgo, the go command, the runtime, -GET /doc/devel/release.html +GET https://golang.org/doc/devel/release.html redirect == /doc/devel/release -GET /doc/faq +GET https://golang.org/doc/faq body contains What is the purpose of the project -GET /doc/gdb +GET https://golang.org/doc/gdb body contains Debugging Go Code -GET /doc/go1.16.html +GET https://golang.org/doc/go1.16.html redirect == /doc/go1.16 -GET /doc/go1.16 +GET https://golang.org/doc/go1.16 body contains Go 1.16 -GET /doc/go_spec +GET https://golang.org/doc/go_spec redirect == /ref/spec -GET /doc/go_spec.html +GET https://golang.org/doc/go_spec.html redirect == /ref/spec -GET /doc/go_spec.md +GET https://golang.org/doc/go_spec.md redirect == /ref/spec -GET /doc/go_mem.html +GET https://golang.org/doc/go_mem.html redirect == /ref/mem -GET /doc/go_mem.md +GET https://golang.org/doc/go_mem.md redirect == /ref/mem -GET /doc/help.html +GET https://golang.org/doc/help.html redirect == /help -GET /help/ +GET https://golang.org/help/ redirect == /help -GET /help +GET https://golang.org/help body contains Get help -GET /pkg/fmt/ +GET https://golang.org/pkg/fmt/ body contains Package fmt implements formatted I/O -GET /src/fmt/ +GET https://golang.org/src/fmt/ body contains scan_test.go -GET /src/fmt/print.go +GET https://golang.org/src/fmt/print.go body contains // Println formats using -GET /pkg +GET https://golang.org/pkg redirect == /pkg/ -GET /pkg/ +GET https://golang.org/pkg/ body contains Standard library body contains Package fmt implements formatted I/O body !contains internal/syscall body !contains cmd/gc -GET /pkg/?m=all +GET https://golang.org/pkg/?m=all body contains Standard library body contains Package fmt implements formatted I/O body contains internal/syscall/?m=all body !contains cmd/gc -GET /pkg/bufio/ +GET https://golang.org/pkg/bufio/ body contains href="/pkg/io/#Writer -GET /pkg/database/sql/ +GET https://golang.org/pkg/database/sql/ body contains The number of connections currently in use; added in Go 1.11 body contains The number of idle connections; added in Go 1.11 -GET /cmd/compile/internal/amd64/ +GET https://golang.org/cmd/compile/internal/amd64/ body contains href="/src/cmd/compile/internal/amd64/ssa.go" -GET /pkg/math/bits/ +GET https://golang.org/pkg/math/bits/ body contains Added in Go 1.9 -GET /pkg/net/ +GET https://golang.org/pkg/net/ body contains // IPv6 scoped addressing zone; added in Go 1.1 -GET /pkg/net/http/ +GET https://golang.org/pkg/net/http/ body contains title="Added in Go 1.11" -GET /pkg/net/http/httptrace/ +GET https://golang.org/pkg/net/http/httptrace/ body ~ Got1xxResponse.*// Go 1\.11 body ~ GotFirstResponseByte func\(\)\s*$ -GET /pkg/os/ +GET https://golang.org/pkg/os/ body contains func Open -GET /pkg/strings/ +GET https://golang.org/pkg/strings/ body contains href="/src/strings/strings.go" -GET /project +GET https://golang.org/project body contains
  • Go 1.14 (February 2020)
  • body contains
  • Go 1.1 (May 2013)
  • -GET /project/ +GET https://golang.org/project/ redirect == /project -GET /project/notexist +GET https://golang.org/project/notexist code == 404 -GET /ref/mem +GET https://golang.org/ref/mem body contains Memory Model -GET /ref/spec +GET https://golang.org/ref/spec body contains Go Programming Language Specification -GET /robots.txt +GET https://golang.org/robots.txt body contains Disallow: /search body !contains UA- -GET /x/net +GET https://golang.org/x/net code == 200 body contains body !contains UA- diff --git a/cmd/golangorg/testdata/x.txt b/cmd/golangorg/testdata/x.txt index 47049268..24233871 100644 --- a/cmd/golangorg/testdata/x.txt +++ b/cmd/golangorg/testdata/x.txt @@ -1,25 +1,25 @@ -GET /x/net +GET https://golang.org/x/net code == 200 body contains body contains http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/net"> -GET /x/net/suffix +GET https://golang.org/x/net/suffix code == 200 body contains body contains http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/net/suffix"> -GET /x/pkgsite +GET https://golang.org/x/pkgsite code == 200 body contains body contains Redirecting to documentation... body contains http-equiv="refresh" content="0; url=https://pkg.go.dev/golang.org/x/pkgsite"> -GET /x/notexist +GET https://golang.org/x/notexist code == 404 -GET /x/ +GET https://golang.org/x/ code == 307 header location == https://pkg.go.dev/search?q=golang.org/x -GET /x/In%20Valid,X +GET https://golang.org/x/In%20Valid,X code == 404 diff --git a/go-app-deploy.sh b/go-app-deploy.sh index 17d036c8..f3da8d1e 100644 --- a/go-app-deploy.sh +++ b/go-app-deploy.sh @@ -6,7 +6,7 @@ # This script is meant to be run from Cloud Build as a substitute # for "gcloud app deploy", as in: # -# go-app-deploy.sh app.yaml +# go-app-deploy.sh [--project=name] app.yaml # # It should not be run by hand and is therefore not marked executable. # @@ -25,18 +25,68 @@ set -e +project=golang-org +case "$1" in +--project=*) + project=$(echo $1 | sed 's/--project=//') + shift +esac + +yaml=app.yaml +case "$1" in +*.yaml) + yaml=$1 + shift +esac + +if [ $# != 0 ]; then + echo 'usage: go-app-deploy.sh [--project=name] path/to/app.yaml' >&2 + exit 2 +fi + promote=$( git cat-file -p 'HEAD' | awk ' - BEGIN { flag = "--no-promote" } - /^Reviewed-on:/ { flag = "--no-promote" } - /^Website-Publish:/ { flag = "--promote" } + BEGIN { flag = "false" } + /^Reviewed-on:/ { flag = "false" } + /^Website-Publish:/ { flag = "true" } END {print flag} ' ) -version=$( - git log -n1 --date='format:%Y-%m-%d-%H%M%S' --pretty='format:%cd-%h' -) +version=$(git log -n1 --date='format:%Y-%m-%d-%H%M%S' --pretty='format:%cd-%h') + +service=$(awk '$1=="service:" {print $2}' $yaml) + +servicedot="-$service-dot" +if [ "$service" = default ]; then + servicedot="" +fi +host="$version-dot$servicedot-$project.appspot.com" + +echo "### deploying to https://$host" +gcloud -q --project=$project app deploy -v $version --no-promote $yaml + +curl --version + +for i in 1 2 3 4 5; do + if curl -s --fail --show-error "https://$host/_readycheck"; then + echo '### site is up!' + if $promote; then + serving=$(gcloud app services describe --project=$project $service | grep ': 1.0') + if [ "$serving" '>' "$version" ]; then + echo "### serving version $serving is newer than our $version; not promoting" + exit 1 + fi + echo '### promoting' + gcloud -q --project=$project app services set-traffic $service --splits=$version=1 + fi + exit 0 + fi + echo '### not healthy' + curl "https://$host/_readycheck" # show response body +done + +echo "### failed to become healthy; giving up" +exit 1 -gcloud app deploy $promote -v $version "$@" diff --git a/go.dev/cmd/frontend/main.go b/go.dev/cmd/frontend/main.go index 7f6d1ae5..db67169e 100644 --- a/go.dev/cmd/frontend/main.go +++ b/go.dev/cmd/frontend/main.go @@ -10,9 +10,11 @@ import ( "net/http" "net/url" "os" + "path/filepath" "strings" "golang.org/x/website/go.dev/cmd/internal/site" + "golang.org/x/website/internal/webtest" ) var discoveryHosts = map[string]string{ @@ -27,13 +29,14 @@ func main() { // Running in repo root. dir = "go.dev" } - godev, err := site.Load(dir) + + h, err := NewHandler(dir) if err != nil { log.Fatal(err) } - http.Handle("/", addCSP(http.FileServer(godev))) - http.Handle("/explore/", http.StripPrefix("/explore/", redirectHosts(discoveryHosts))) - http.Handle("learn.go.dev/", http.HandlerFunc(redirectLearn)) + + h = webtest.HandlerWithCheck(h, "/_readycheck", + filepath.Join(dir, "cmd/frontend/testdata/*.txt")) addr := ":" + listenPort() if addr == ":0" { @@ -45,7 +48,19 @@ func main() { } defer l.Close() log.Printf("Listening on http://%v/\n", l.Addr().String()) - log.Print(http.Serve(l, nil)) + log.Print(http.Serve(l, h)) +} + +func NewHandler(dir string) (http.Handler, error) { + godev, err := site.Load(dir) + if err != nil { + return nil, err + } + mux := http.NewServeMux() + mux.Handle("/", addCSP(http.FileServer(godev))) + mux.Handle("/explore/", http.StripPrefix("/explore/", redirectHosts(discoveryHosts))) + mux.Handle("learn.go.dev/", http.HandlerFunc(redirectLearn)) + return mux, nil } func redirectLearn(w http.ResponseWriter, r *http.Request) { diff --git a/go.dev/cmd/frontend/server_test.go b/go.dev/cmd/frontend/server_test.go new file mode 100644 index 00000000..831838a0 --- /dev/null +++ b/go.dev/cmd/frontend/server_test.go @@ -0,0 +1,19 @@ +// 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 ( + "testing" + + "golang.org/x/website/internal/webtest" +) + +func TestWeb(t *testing.T) { + h, err := NewHandler("../..") + if err != nil { + t.Fatal(err) + } + webtest.TestHandler(t, "testdata/*.txt", h) +} diff --git a/go.dev/cmd/frontend/testdata/godev.txt b/go.dev/cmd/frontend/testdata/godev.txt new file mode 100644 index 00000000..950b385e --- /dev/null +++ b/go.dev/cmd/frontend/testdata/godev.txt @@ -0,0 +1,5 @@ +GET https://go.dev/ +body contains

    Companies using Go

    + +GET https://go.dev/solutions/google/ +body ~ it\s+has\s+powered\s+many\s+projects\s+at\s+Google. diff --git a/internal/dl/server.go b/internal/dl/server.go index 43cc1164..dfb8b036 100644 --- a/internal/dl/server.go +++ b/internal/dl/server.go @@ -115,26 +115,6 @@ func serveJSON(w http.ResponseWriter, r *http.Request, d listTemplateData) { } } -// googleCN reports whether request r is considered -// to be served from golang.google.cn. -// TODO: This is duplicated within internal/proxy. Move to a common location. -func googleCN(r *http.Request) bool { - if r.FormValue("googlecn") != "" { - return true - } - if strings.HasSuffix(r.Host, ".cn") { - return true - } - if !env.CheckCountry() { - return false - } - switch r.Header.Get("X-Appengine-Country") { - case "", "ZZ", "CN": - return true - } - return false -} - func (h server) uploadHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) diff --git a/internal/env/env.go b/internal/env/env.go index 80575a3d..3eca79cb 100644 --- a/internal/env/env.go +++ b/internal/env/env.go @@ -13,7 +13,6 @@ import ( ) var ( - checkCountry = boolEnv("GOLANGORG_CHECK_COUNTRY") enforceHosts = boolEnv("GOLANGORG_ENFORCE_HOSTS") requireDLSecretKey = boolEnv("GOLANGORG_REQUIRE_DL_SECRET_KEY") ) @@ -25,11 +24,6 @@ func RequireDLSecretKey() bool { return requireDLSecretKey } -// CheckCountry reports whether country restrictions should be enforced. -func CheckCountry() bool { - return checkCountry -} - // EnforceHosts reports whether host filtering should be enforced. func EnforceHosts() bool { return enforceHosts diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go index 2843a77b..2584b362 100644 --- a/internal/proxy/proxy.go +++ b/internal/proxy/proxy.go @@ -15,10 +15,9 @@ import ( "io/ioutil" "log" "net/http" - "strings" "time" - "golang.org/x/website/internal/env" + "golang.org/x/website/internal/web" ) const playgroundURL = "https://play.golang.org" @@ -124,7 +123,7 @@ func flatten(seq []Event) string { } func share(w http.ResponseWriter, r *http.Request) { - if googleCN(r) { + if web.GoogleCN(r) { http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } @@ -151,22 +150,3 @@ func share(w http.ResponseWriter, r *http.Request) { w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) } - -// googleCN reports whether request r is considered -// to be served from golang.google.cn. -func googleCN(r *http.Request) bool { - if r.FormValue("googlecn") != "" { - return true - } - if strings.HasSuffix(r.Host, ".cn") { - return true - } - if !env.CheckCountry() { - return false - } - switch r.Header.Get("X-Appengine-Country") { - case "", "ZZ", "CN": - return true - } - return false -} diff --git a/internal/web/googlecn.go b/internal/web/googlecn.go new file mode 100644 index 00000000..82d658ba --- /dev/null +++ b/internal/web/googlecn.go @@ -0,0 +1,28 @@ +// 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 web + +import ( + "net/http" + "strings" +) + +// GoogleCN reports whether request r is considered to be arriving from China. +// Typically that means the request is for host golang.google.cn, +// but we also report true for requests that set googlecn=1 as a query parameter +// and requests that App Engine geolocates in China or in “unknown country.” +func GoogleCN(r *http.Request) bool { + if r.FormValue("googlecn") != "" { + return true + } + if strings.HasSuffix(r.Host, ".cn") { + return true + } + switch r.Header.Get("X-Appengine-Country") { + case "ZZ", "CN": + return true + } + return false +} diff --git a/internal/web/site.go b/internal/web/site.go index 4383eee5..245c99ea 100644 --- a/internal/web/site.go +++ b/internal/web/site.go @@ -36,10 +36,6 @@ type Site struct { Templates *template.Template - // GoogleCN reports whether this request should be marked GoogleCN. - // If the function is nil, no requests are marked GoogleCN. - GoogleCN func(*http.Request) bool - // GoogleAnalytics optionally adds Google Analytics via the provided // tracking ID to each page. GoogleAnalytics string @@ -122,7 +118,7 @@ type Page struct { Data interface{} // data to be rendered into page frame // Filled in automatically by ServePage - GoogleCN bool // page is being served from golang.google.cn + GoogleCN bool // served on golang.google.cn GoogleAnalytics string // Google Analytics tag Version string // current Go version @@ -135,7 +131,7 @@ func (s *Site) fullPage(r *http.Request, page Page) Page { page.TabTitle = page.Title } page.Version = runtime.Version() - page.GoogleCN = s.googleCN(r) + page.GoogleCN = GoogleCN(r) page.GoogleAnalytics = s.GoogleAnalytics page.site = s return page @@ -413,7 +409,3 @@ func (s *Site) serveRawText(w http.ResponseWriter, text []byte) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write(text) } - -func (s *Site) googleCN(r *http.Request) bool { - return s.GoogleCN != nil && s.GoogleCN(r) -} diff --git a/internal/webtest/webtest.go b/internal/webtest/webtest.go index 27765f87..6a6e29e4 100644 --- a/internal/webtest/webtest.go +++ b/internal/webtest/webtest.go @@ -164,6 +164,26 @@ import ( "unicode/utf8" ) +// HandlerWithCheck returns an http.Handler that responds to each request +// by running the test script files mached by glob against the handler h. +// If the tests pass, the returned http.Handler responds with status code 200. +// If they fail, it prints the details and responds with status code 503 +// (service unavailable). +func HandlerWithCheck(h http.Handler, path, glob string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == path { + err := CheckHandler(glob, h) + if err != nil { + http.Error(w, "webtest.CheckHandler failed:\n"+err.Error()+"\n", http.StatusInternalServerError) + } else { + fmt.Fprintf(w, "ok\n") + } + return + } + h.ServeHTTP(w, r) + }) +} + // CheckHandler runs the test script files matched by glob // against the handler h. If any errors are encountered, // CheckHandler returns an error listing the problems. diff --git a/tour/appengine.go b/tour/appengine.go index fffc9e86..0f02356a 100644 --- a/tour/appengine.go +++ b/tour/appengine.go @@ -12,8 +12,10 @@ import ( "log" "net/http" "os" + "path/filepath" _ "golang.org/x/tools/playground" + "golang.org/x/website/internal/webtest" ) func gaeMain() { @@ -36,7 +38,11 @@ func gaeMain() { if port == "" { port = "8080" } - log.Fatal(http.ListenAndServe(":"+port, nil)) + + h := webtest.HandlerWithCheck(http.DefaultServeMux, "/_readycheck", + filepath.Join(root, "testdata/*.txt")) + + log.Fatal(http.ListenAndServe(":"+port, h)) } // gaePrepContent returns a Reader that produces the content from the given diff --git a/tour/local.go b/tour/local.go index d64417e7..f59edfa0 100644 --- a/tour/local.go +++ b/tour/local.go @@ -23,6 +23,7 @@ import ( "time" "golang.org/x/tools/playground/socket" + "golang.org/x/website/internal/webtest" ) const ( @@ -126,6 +127,9 @@ func main() { registerStatic(root) + h := webtest.HandlerWithCheck(http.DefaultServeMux, "/_readycheck", + filepath.Join(root, "testdata/*.txt")) + go func() { url := "http://" + httpAddr if waitServer(url) && *openBrowser && startBrowser(url) { @@ -134,7 +138,7 @@ func main() { log.Printf("Please open your web browser and visit %s", url) } }() - log.Fatal(http.ListenAndServe(httpAddr, nil)) + log.Fatal(http.ListenAndServe(httpAddr, h)) } // registerStatic registers handlers to serve static content diff --git a/tour/server_test.go b/tour/server_test.go new file mode 100644 index 00000000..acc6ee30 --- /dev/null +++ b/tour/server_test.go @@ -0,0 +1,24 @@ +// 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 ( + "log" + "net/http" + "testing" + + "golang.org/x/website/internal/webtest" +) + +func TestWeb(t *testing.T) { + if err := initTour(".", "SocketTransport"); err != nil { + log.Fatal(err) + } + http.HandleFunc("/", rootHandler) + http.HandleFunc("/lesson/", lessonHandler) + registerStatic(".") + + webtest.TestHandler(t, "testdata/*.txt", http.DefaultServeMux) +} diff --git a/tour/testdata/tour.txt b/tour/testdata/tour.txt new file mode 100644 index 00000000..bb1db633 --- /dev/null +++ b/tour/testdata/tour.txt @@ -0,0 +1,3 @@ +GET https://tour.golang.org/ +body contains >A Tour of Go< +