devapp: initial support for App Engine Flex
devapp now supports 3 modes of operation:
- App Engine Standard. This supports Go 1.6 and requires a modified
local go-github to continue working. This app is built with the
"appengine" build tag.
- App Engine Flex. This builds Go 1.8 in a Docker container, using the
build tag "appenginevm", and works with the Google Cloud platform.
- Normal. This uses an in-memory datastore, and does not
support user login/logout.
To get this working I copied appengine.go to noappengine.go and
modified the calls until they were working. You can view a running app
at https://devapp-161505.appspot.com.
Change-Id: I8ea018e63baf2dafb44150f7eee419e09065ba2c
Reviewed-on: https://go-review.googlesource.com/38161
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/cmd/devapp/main.go b/cmd/devapp/main.go
deleted file mode 100644
index 0f41a48..0000000
--- a/cmd/devapp/main.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// 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.
-
-// Devapp generates the dashboard that powers dev.golang.org.
-//
-// Usage:
-//
-// devapp --port=8081
-//
-// By default devapp listens on port 8081.
-//
-// Github issues and Gerrit CL's are stored in memory in the running process.
-// To trigger an initial download, visit http://localhost:8081/update or
-// http://localhost:8081/update/stats in your browser.
-package main
-
-import (
- "flag"
- "fmt"
- "log"
- "net"
- "net/http"
- "os"
-
- _ "golang.org/x/build/devapp"
-)
-
-func init() {
- flag.Usage = func() {
- os.Stderr.WriteString(`usage: devapp [-port=port]
-
-Devapp generates the dashboard that powers dev.golang.org.
-`)
- os.Exit(2)
- }
-}
-
-func main() {
- port := flag.Uint("port", 8081, "Port to listen on")
- flag.Parse()
- ln, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
- if err != nil {
- log.Fatal(err)
- }
- fmt.Fprintf(os.Stderr, "Listening on port %d\n", *port)
- log.Fatal(http.Serve(ln, nil))
-}
diff --git a/devapp/appengine.go b/devapp/appengine.go
index 3d56980..e0b5578 100644
--- a/devapp/appengine.go
+++ b/devapp/appengine.go
@@ -18,23 +18,22 @@
)
func init() {
- onAppengine = !appengine.IsDevAppServer()
- log = &appengineLogger{}
+ logger = appengineLogger{}
http.HandleFunc("/setToken", setTokenHandler)
}
type appengineLogger struct{}
-func (a *appengineLogger) Infof(ctx context.Context, format string, args ...interface{}) {
+func (a appengineLogger) Infof(ctx context.Context, format string, args ...interface{}) {
applog.Infof(ctx, format, args...)
}
-func (a *appengineLogger) Errorf(ctx context.Context, format string, args ...interface{}) {
+func (a appengineLogger) Errorf(ctx context.Context, format string, args ...interface{}) {
applog.Errorf(ctx, format, args...)
}
-func (a *appengineLogger) Criticalf(ctx context.Context, format string, args ...interface{}) {
+func (a appengineLogger) Criticalf(ctx context.Context, format string, args ...interface{}) {
applog.Criticalf(ctx, format, args...)
}
@@ -108,19 +107,6 @@
return string(cache.Value), nil
}
-// Store a token in the database
-func setTokenHandler(w http.ResponseWriter, r *http.Request) {
- ctx := appengine.NewContext(r)
- r.ParseForm()
- if value := r.Form.Get("value"); value != "" {
- var token Cache
- token.Value = []byte(value)
- if err := putCache(ctx, "github-token", &token); err != nil {
- http.Error(w, err.Error(), 500)
- }
- }
-}
-
func getContext(r *http.Request) context.Context {
return appengine.NewContext(r)
}
diff --git a/devapp/appenginevm.go b/devapp/appenginevm.go
new file mode 100644
index 0000000..275f0bf
--- /dev/null
+++ b/devapp/appenginevm.go
@@ -0,0 +1,149 @@
+// 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.
+
+// This file gets built on App Engine Flex.
+
+// +build appenginevm
+
+package devapp
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+
+ "cloud.google.com/go/compute/metadata"
+ "cloud.google.com/go/datastore"
+ "cloud.google.com/go/logging"
+ "golang.org/x/net/context"
+)
+
+var lg *logging.Logger
+var dsClient *datastore.Client
+
+func init() {
+ logger = appengineLogger{}
+ id, err := metadata.ProjectID()
+ if err != nil {
+ id = "devapp"
+ }
+ ctx := context.Background()
+ client, err := logging.NewClient(ctx, id)
+ if err == nil {
+ lg = client.Logger("log")
+ }
+ dsClient, _ = datastore.NewClient(ctx, id)
+
+ http.Handle("/static/", http.FileServer(http.Dir(".")))
+ http.HandleFunc("/favicon.ico", faviconHandler)
+ http.HandleFunc("/setToken", setTokenHandler)
+}
+
+type appengineLogger struct{}
+
+func (a appengineLogger) Infof(_ context.Context, format string, args ...interface{}) {
+ if lg == nil {
+ return
+ }
+ lg.Log(logging.Entry{
+ Severity: logging.Info,
+ Payload: fmt.Sprintf(format, args...),
+ })
+}
+
+func (a appengineLogger) Errorf(_ context.Context, format string, args ...interface{}) {
+ if lg == nil {
+ return
+ }
+ lg.Log(logging.Entry{
+ Severity: logging.Error,
+ Payload: fmt.Sprintf(format, args...),
+ })
+}
+
+func (a appengineLogger) Criticalf(ctx context.Context, format string, args ...interface{}) {
+ if lg == nil {
+ return
+ }
+ lg.LogSync(ctx, logging.Entry{
+ Severity: logging.Critical,
+ Payload: fmt.Sprintf(format, args...),
+ })
+}
+
+func newTransport(ctx context.Context) http.RoundTripper {
+ // This doesn't have a context, but we should be setting it on the request
+ // when it comes through.
+ return &http.Transport{}
+}
+
+func currentUserEmail(ctx context.Context) string {
+ return ""
+}
+
+// loginURL returns a URL that, when visited, prompts the user to sign in,
+// then redirects the user to the URL specified by dest.
+func loginURL(ctx context.Context, path string) (string, error) {
+ return "", errors.New("unimplemented")
+}
+
+func logoutURL(ctx context.Context, path string) (string, error) {
+ return "", errors.New("unimplemented")
+}
+
+func getCache(ctx context.Context, name string) (*Cache, error) {
+ cache := new(Cache)
+ key := datastore.NameKey(entityPrefix+"Cache", name, nil)
+ if err := dsClient.Get(ctx, key, cache); err != nil {
+ return cache, err
+ }
+ return cache, nil
+}
+
+func getCaches(ctx context.Context, names ...string) map[string]*Cache {
+ out := make(map[string]*Cache)
+ var keys []*datastore.Key
+ var ptrs []*Cache
+ for _, name := range names {
+ keys = append(keys, datastore.NameKey(entityPrefix+"Cache", name, nil))
+ out[name] = new(Cache)
+ ptrs = append(ptrs, out[name])
+ }
+ dsClient.GetMulti(ctx, keys, ptrs) // Ignore errors since they might not exist.
+ return out
+}
+
+func getPage(ctx context.Context, page string) (*Page, error) {
+ entity := new(Page)
+ key := datastore.NameKey(entityPrefix+"Page", page, nil)
+ err := dsClient.Get(ctx, key, entity)
+ return entity, err
+}
+
+func writePage(ctx context.Context, page string, content []byte) error {
+ entity := &Page{
+ Content: content,
+ }
+ key := datastore.NameKey(entityPrefix+"Page", page, nil)
+ _, err := dsClient.Put(ctx, key, entity)
+ return err
+}
+
+func putCache(ctx context.Context, name string, c *Cache) error {
+ key := datastore.NameKey(entityPrefix+"Cache", name, nil)
+ _, err := dsClient.Put(ctx, key, c)
+ return err
+}
+
+func getToken(ctx context.Context) (string, error) {
+ cache, err := getCache(ctx, "github-token")
+ if err != nil {
+ return "", err
+ }
+ return string(cache.Value), nil
+}
+
+func getContext(r *http.Request) context.Context {
+ return r.Context()
+}
diff --git a/devapp/appenginevm_test.go b/devapp/appenginevm_test.go
new file mode 100644
index 0000000..61ddce7
--- /dev/null
+++ b/devapp/appenginevm_test.go
@@ -0,0 +1,22 @@
+// 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.
+
+// +build appenginevm
+
+package devapp
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "testing"
+)
+
+func TestSetTokenMethodNotAllowed(t *testing.T) {
+ w := httptest.NewRecorder()
+ req := httptest.NewRequest("GET", "/setToken", nil)
+ http.DefaultServeMux.ServeHTTP(w, req)
+ if w.Code != 405 {
+ t.Errorf("GET /setToken: got %d, want 405", w.Code)
+ }
+}
diff --git a/devapp/cache.go b/devapp/cache.go
index 2bfd49a..22b1956 100644
--- a/devapp/cache.go
+++ b/devapp/cache.go
@@ -17,7 +17,7 @@
// Value contains a gzipped gob'd serialization of the object
// to be cached. It must be []byte to avail ourselves of the
// datastore's 1 MB size limit.
- Value []byte
+ Value []byte `datastore:"value,noindex"`
}
func unpackCache(cache *Cache, data interface{}) error {
@@ -54,6 +54,6 @@
return err
}
cache.Value = cacheout.Bytes()
- log.Infof(ctx, "Cache %q update finished; writing %d bytes", name, cacheout.Len())
+ logger.Infof(ctx, "Cache %q update finished; writing %d bytes", name, cacheout.Len())
return putCache(ctx, name, &cache)
}
diff --git a/devapp/dash.go b/devapp/dash.go
index 6e51c91..c26c104 100644
--- a/devapp/dash.go
+++ b/devapp/dash.go
@@ -18,15 +18,15 @@
"golang.org/x/net/context"
)
-var onAppengine = false
-
-type logger interface {
+type pkgLogger interface {
+ // This needs to be x/net/context because App Engine Standard still runs on
+ // Go 1.6.
Infof(context.Context, string, ...interface{})
Errorf(context.Context, string, ...interface{})
Criticalf(context.Context, string, ...interface{})
}
-var log logger
+var logger pkgLogger
func findEmail(ctx context.Context, data *godash.Data) string {
email := currentUserEmail(ctx)
@@ -118,7 +118,7 @@
tmpl, err := ioutil.ReadFile("template/dash.html")
if err != nil {
- log.Errorf(ctx, "reading template: %v", err)
+ logger.Errorf(ctx, "reading template: %v", err)
return
}
t, err := template.New("main").Funcs(template.FuncMap{
@@ -135,7 +135,7 @@
"release": d.release,
}).Parse(string(tmpl))
if err != nil {
- log.Errorf(ctx, "parsing template: %v", err)
+ logger.Errorf(ctx, "parsing template: %v", err)
return
}
@@ -174,7 +174,7 @@
}
if err := t.Execute(w, tData); err != nil {
- log.Errorf(ctx, "execute: %v", err)
+ logger.Errorf(ctx, "execute: %v", err)
http.Error(w, "error executing template", 500)
return
}
diff --git a/devapp/devapp.go b/devapp/devapp.go
index 0ed9ef9..88e247d 100644
--- a/devapp/devapp.go
+++ b/devapp/devapp.go
@@ -11,7 +11,7 @@
"bytes"
"fmt"
"io"
- stdlog "log"
+ "log"
"net/http"
"strings"
"sync/atomic"
@@ -53,23 +53,23 @@
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := getContext(r)
if err := fn(ctx, w, r); err != nil {
- log.Criticalf(ctx, "handler failed: %v", err)
+ logger.Criticalf(ctx, "handler failed: %v", err)
http.Error(w, err.Error(), 500)
}
})
}
func logFn(ctx context.Context, w io.Writer) func(string, ...interface{}) {
- logger := stdlog.New(w, "", stdlog.Lmicroseconds)
+ stdLogger := log.New(w, "", log.Lmicroseconds)
return func(format string, args ...interface{}) {
- logger.Printf(format, args...)
- log.Infof(ctx, format, args...)
+ stdLogger.Printf(format, args...)
+ logger.Infof(ctx, format, args...)
}
}
type Page struct {
// Content is the complete HTML of the page.
- Content []byte
+ Content []byte `datastore:"content,noindex"`
}
func servePage(w http.ResponseWriter, r *http.Request, page string) {
@@ -106,7 +106,7 @@
ct := &countTransport{newTransport(ctx), 0}
gh := godash.NewGitHubClient("golang/go", token, ct)
defer func() {
- log.Infof(ctx, "Sent %d requests to GitHub", ct.Count())
+ logger.Infof(ctx, "Sent %d requests to GitHub", ct.Count())
}()
ger := gerrit.NewClient("https://go-review.googlesource.com", gerrit.NoAuth)
// Without a deadline, urlfetch will use a 5s timeout which is too slow for Gerrit.
@@ -120,12 +120,12 @@
}
if err := data.Reviewers.LoadGithub(ctx, gh); err != nil {
- log.Criticalf(ctx, "failed to load reviewers: %v", err)
+ logger.Criticalf(ctx, "failed to load reviewers: %v", err)
return err
}
l := logFn(ctx, w)
if err := data.FetchData(gerctx, gh, ger, l, 7, false, false); err != nil {
- log.Criticalf(ctx, "failed to fetch data: %v", err)
+ logger.Criticalf(ctx, "failed to fetch data: %v", err)
return err
}
@@ -153,3 +153,30 @@
}
return writeCache(ctx, "gzdata", &data)
}
+
+// POST /setToken
+//
+// Store a github token in the database, so we can use it for API calls.
+// Necessary because the only available configuration method is the app.yaml
+// file, which we want to check in, and can't store secrets.
+func setTokenHandler(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ w.Header().Set("Allow", "POST")
+ http.Error(w, "Method not allowed", 405)
+ return
+ }
+ ctx := r.Context()
+ r.ParseForm()
+ if value := r.Form.Get("value"); value != "" {
+ var token Cache
+ token.Value = []byte(value)
+ if err := putCache(ctx, "github-token", &token); err != nil {
+ http.Error(w, err.Error(), 500)
+ }
+ }
+}
+
+// GET /favicon.ico
+func faviconHandler(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, "./static/favicon.ico")
+}
diff --git a/devapp/devapp_test.go b/devapp/devapp_test.go
index 43755f8..2244d6a 100644
--- a/devapp/devapp_test.go
+++ b/devapp/devapp_test.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// +build !appenginevm
+
package devapp
import (
diff --git a/devapp/devappserver/app.yaml b/devapp/devappserver/app.yaml
new file mode 100644
index 0000000..09d1ac3
--- /dev/null
+++ b/devapp/devappserver/app.yaml
@@ -0,0 +1,13 @@
+# YAML file necessary to deploy to App Engine Flex.
+#
+# To deploy, run "gcloud app deploy". You will need to sign up for an account
+# and configure your local gcloud settings.
+#
+# As of March 2017 deployments take 4-8 minutes (to upload all files, build
+# a Docker container, and provision that container onto multiple servers). Note
+# that libraries will be deployed as they exist on your local filesystem, so
+# ensure third party dependencies are up to date.
+#
+# More info: https://cloud.google.com/appengine/docs/flexible/go/quickstart
+runtime: go
+env: flex
diff --git a/devapp/devappserver/main.go b/devapp/devappserver/main.go
new file mode 100644
index 0000000..e623bcf
--- /dev/null
+++ b/devapp/devappserver/main.go
@@ -0,0 +1,66 @@
+// 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.
+
+// Devapp generates the dashboard that powers dev.golang.org. This binary is
+// designed to be run outside of an App Engine context.
+//
+// Usage:
+//
+// devappserver --port=8081
+//
+// By default devapp listens on port 8081. You can also configure the port by
+// setting the PORT environment variable (but not both).
+//
+// For the moment, Github issues and Gerrit CL's are stored in memory
+// in the running process. To trigger an initial download, visit
+// http://localhost:8081/update and/or http://localhost:8081/update/stats in
+// your browser.
+
+package main
+
+import (
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+
+ _ "golang.org/x/build/devapp" // registers HTTP handlers
+)
+
+func init() {
+ flag.Usage = func() {
+ os.Stderr.WriteString(`usage: devapp [-port=port]
+
+Devapp generates the dashboard that powers dev.golang.org.
+ `)
+ }
+}
+
+const defaultPort = "8081"
+
+func main() {
+ portFlag := flag.String("port", "", "Port to listen on")
+ flag.Parse()
+ if *portFlag != "" && os.Getenv("PORT") != "" {
+ os.Stderr.WriteString("cannot set both $PORT and --port flags\n")
+ os.Exit(2)
+ }
+ var port string
+ if p := os.Getenv("PORT"); p != "" {
+ port = p
+ } else if *portFlag != "" {
+ port = *portFlag
+ } else {
+ port = defaultPort
+ }
+ ln, err := net.Listen("tcp", ":"+port)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error listening on %s: %v\n", port, err)
+ os.Exit(2)
+ }
+ fmt.Fprintf(os.Stderr, "Listening on port %s\n", port)
+ log.Fatal(http.Serve(ln, nil))
+}
diff --git a/devapp/noappengine.go b/devapp/noappengine.go
index 292a84f..759b12d 100644
--- a/devapp/noappengine.go
+++ b/devapp/noappengine.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+// +build !appenginevm
// +build !appengine
package devapp
@@ -25,7 +26,7 @@
var tokenFile = flag.String("token", "", "read GitHub token personal access token from `file` (default $HOME/.github-issue-token)")
func init() {
- log = &stderrLogger{}
+ logger = &stderrLogger{}
// TODO don't bind to the working directory.
http.Handle("/static/", http.FileServer(http.Dir(".")))
http.HandleFunc("/favicon.ico", faviconHandler)
@@ -171,8 +172,3 @@
func getContext(r *http.Request) context.Context {
return r.Context()
}
-
-func faviconHandler(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "image/x-icon")
- http.ServeFile(w, r, "./static/favicon.ico")
-}