Add talk for Minneapolis Ultimate Go

pull/10616/head
Chris Goller 2017-03-16 10:01:23 -05:00
parent 733125b480
commit 295388cd87
13 changed files with 681 additions and 0 deletions

110
docs/slides/mnGo/Makefile Normal file
View File

@ -0,0 +1,110 @@
.PHONY: assets dep clean test gotest gotestrace jstest run run-dev ctags continuous
VERSION ?= $(shell git describe --always --tags)
COMMIT ?= $(shell git rev-parse --short=8 HEAD)
GDM := $(shell command -v gdm 2> /dev/null)
GOBINDATA := $(shell go list -f {{.Root}} github.com/jteeuwen/go-bindata 2> /dev/null)
YARN := $(shell command -v yarn 2> /dev/null)
SOURCES := $(shell find . -name '*.go' ! -name '*_gen.go')
UISOURCES := $(shell find ui -type f -not \( -path ui/build/\* -o -path ui/node_modules/\* -prune \) )
LDFLAGS=-ldflags "-s -X main.version=${VERSION} -X main.commit=${COMMIT}"
BINARY=chronograf
.DEFAULT_GOAL := all
all: dep build
build: assets ${BINARY}
dev: dep dev-assets ${BINARY}
${BINARY}: $(SOURCES) .bindata .jsdep .godep
go build -o ${BINARY} ${LDFLAGS} ./cmd/chronograf/main.go
docker-${BINARY}: $(SOURCES)
CGO_ENABLED=0 GOOS=linux go build -installsuffix cgo -o ${BINARY} ${LDFLAGS} \
./cmd/chronograf/main.go
docker: dep assets docker-${BINARY}
docker build -t chronograf .
assets: .jssrc .bindata
dev-assets: .dev-jssrc .bindata
.bindata: server/swagger_gen.go canned/bin_gen.go dist/dist_gen.go
@touch .bindata
dist/dist_gen.go: $(UISOURCES)
go generate -x ./dist
server/swagger_gen.go: server/swagger.json
go generate -x ./server
canned/bin_gen.go: canned/*.json
go generate -x ./canned
.jssrc: $(UISOURCES)
cd ui && npm run build
@touch .jssrc
.dev-jssrc: $(UISOURCES)
cd ui && npm run build:dev
@touch .dev-jssrc
dep: .jsdep .godep
.godep: Godeps
ifndef GDM
@echo "Installing GDM"
go get github.com/sparrc/gdm
endif
ifndef GOBINDATA
@echo "Installing go-bindata"
go get -u github.com/jteeuwen/go-bindata/...
endif
gdm restore
@touch .godep
.jsdep: ui/yarn.lock
ifndef YARN
$(error Please install yarn 0.19.1+)
else
cd ui && yarn --no-progress --no-emoji
@touch .jsdep
endif
gen: bolt/internal/internal.proto
go generate -x ./bolt/internal
test: jstest gotest gotestrace
gotest:
go test ./...
gotestrace:
go test -race ./...
jstest:
cd ui && npm test
run: ${BINARY}
./chronograf
run-dev: ${BINARY}
./chronograf -d --log-level=debug
clean:
if [ -f ${BINARY} ] ; then rm ${BINARY} ; fi
cd ui && npm run clean
cd ui && rm -rf node_modules
rm -f dist/dist_gen.go canned/bin_gen.go server/swagger_gen.go
@rm -f .godep .jsdep .jssrc .dev-jssrc .bindata
continuous:
while true; do if fswatch -r --one-event .; then echo "#-> Starting build: `date`"; make dev; pkill chronograf; ./chronograf -d --log-level=debug & echo "#-> Build complete."; fi; sleep 0.5; done
ctags:
ctags -R --languages="Go" --exclude=.git --exclude=ui .

View File

@ -0,0 +1,28 @@
// +build OMIT
package oauth2
import (
"context"
"net/http"
)
func AuthorizedToken(auth Authenticator, te TokenExtractor, next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token, err := te.Extract(r)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
principal, err := auth.Authenticate(r.Context(), token)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
// Send the principal to the next handler for further authorization
ctx := context.WithValue(r.Context(), PrincipalKey, principal)
next.ServeHTTP(w, r.WithContext(ctx))
return
})
}

View File

@ -0,0 +1,15 @@
// +build OMIT
package dist
import (
"fmt"
"net/http"
)
func (b *BindataAssets) addCacheHeaders(filename string, w http.ResponseWriter) error {
w.Header().Add("Cache-Control", "public, max-age=3600")
fi, _ := AssetInfo(filename)
hour, minute, second := fi.ModTime().Clock()
etag := fmt.Sprintf(`"%d%d%d%d%d"`, fi.Size(), fi.ModTime().Day(), hour, minute, second)
w.Header().Set("ETag", etag)
}

View File

@ -0,0 +1,108 @@
Serving React with light-weight Go
Lessons learned while building Chronograf
19:00 15 Mar 2017
Tags: react, go, chronograf
Chris Goller
Architect, InfluxData
chris@influxdb.com
@goller
* Demo
- Source: [[https://github.com/influxdata/chronograf]]
- [[http://localhost:8888]]
* Chronograf Goals
- Single Page Application
- Install to Productivity in 2 mins
- Familiar dev environment for Javascript _and_ Go
* Agenda
- Walking through the middleware stack
- Satisfying both Go and Javascript developers
* Simplest File Server
.code simple/router.go
- However, React routing needs "wildcard" route to return same HTML page.
- Client-Side Routing!
* React routing
- simple routing in react applications use fragments
- but if the server supports wildcarding then we can get nice looking routes
* Wildcard routing to default asset
.code net/http/fs.go /func FileServer/,/^}/
Implement `FileSystem` with a default file
.code simple/dir.go /type/,/OMIT END/
* Minimal React Host Server
.code simple/react.go
* Problems with SPA
- Slow loads
- Typically, not scrapable
* Caching
- Cache-Control: [[https://tools.ietf.org/html/rfc7234#section-5.2]]
- ETag: [[https://tools.ietf.org/html/rfc7232#section-2.3]]
.code caching/dist.go /w\./,/^}/
- Strong ETag validation
* Compression
- gzip middleware by `github.com/NYTimes/gziphandler`
.code gzip/mux.go
* Authentication and Authorization
- If authenticated able to see assets else redirected to /login
- If authorized able to read parts of REST API
.code auth/auth.go /func AuthorizedToken/,/^}/
* Bonus middleware :)
* HSTS
.code hsts/hsts.go /HSTS/,/^}/
- informs the client to cache that HTTPS should be used for a length of time.
- if client receives HTTP instead reject as a possible man-in-the-middle.
- HSTS doesn't redirect HTTP to HTTPS but rather is only used on HTTPS responses.
* Version
.code version/version.go /func Version/,/^}/
- Not necessarily directly useful for React
- Nice for debugging
* Asset Rewriting
- index.html generally comes from /
- Operationally, nice to serve from different routes
- Better way? Help!
.code prefixer/url_prefixer.go /\*URLPrefixer/,/^}/
* Development Environment
- One repository
- "Native" tooling for all developers
- Demo
- One build system to rule them all... make!
* Makefile
- Get all vendoring depedencies and build everything!
make
- Test Go and Javascript
make test
- Run Go server
make run
- Continuous builds of the Go server
make continuous

View File

@ -0,0 +1,19 @@
// +build OMIT
package server
import (
"net/http"
"github.com/NYTimes/gziphandler"
"github.com/bouk/httprouter"
)
func NewMux(opts MuxOpts, service Service) http.Handler {
router := httprouter.New()
...
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
// Compress the assets with gzip if an accepted encoding
compressed := gziphandler.GzipHandler(prefixedAssets)
...
return handler
}

View File

@ -0,0 +1,13 @@
// +build OMIT
package server
import "net/http"
// HSTS add HTTP Strict Transport Security header with a max-age of two years
// Inspired from https://blog.bracebin.com/achieving-perfect-ssl-labs-score-with-go
func HSTS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
next.ServeHTTP(w, r)
})
}

248
docs/slides/mnGo/mux/mux.go Normal file
View File

@ -0,0 +1,248 @@
// +build OMIT
package server
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/NYTimes/gziphandler"
"github.com/bouk/httprouter"
"github.com/influxdata/chronograf" // When julienschmidt/httprouter v2 w/ context is out, switch
"github.com/influxdata/chronograf/oauth2"
)
const (
// JSONType the mimetype for a json request
JSONType = "application/json"
)
// MuxOpts are the options for the router. Mostly related to auth.
type MuxOpts struct {
Logger chronograf.Logger
Develop bool // Develop loads assets from filesystem instead of bindata
Basepath string // URL path prefix under which all chronograf routes will be mounted
UseAuth bool // UseAuth turns on Github OAuth and JWT
TokenSecret string
ProviderFuncs []func(func(oauth2.Provider, oauth2.Mux))
}
// NewMux attaches all the route handlers; handler returned servers chronograf.
func NewMux(opts MuxOpts, service Service) http.Handler {
router := httprouter.New()
/* React Application */
assets := Assets(AssetsOpts{
Develop: opts.Develop,
Logger: opts.Logger,
})
// Prefix any URLs found in the React assets with any configured basepath
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
// Compress the assets with gzip if an accepted encoding
compressed := gziphandler.GzipHandler(prefixedAssets)
// The react application handles all the routing if the server does not
// know about the route. This means that we never have unknown
// routes on the server.
router.NotFound = compressed
/* Documentation */
router.GET("/swagger.json", Spec())
router.GET("/docs", Redoc("/swagger.json"))
/* API */
// Sources
// OMIT BEGIN
router.GET("/chronograf/v1/sources", service.Sources)
router.POST("/chronograf/v1/sources", service.NewSource)
router.GET("/chronograf/v1/sources/:id", service.SourcesID)
router.PATCH("/chronograf/v1/sources/:id", service.UpdateSource)
router.DELETE("/chronograf/v1/sources/:id", service.RemoveSource)
// OMIT END
// Source Proxy to Influx
router.POST("/chronograf/v1/sources/:id/proxy", service.Influx)
// All possible permissions for users in this source
router.GET("/chronograf/v1/sources/:id/permissions", service.Permissions)
// Users associated with the data source
router.GET("/chronograf/v1/sources/:id/users", service.SourceUsers)
router.POST("/chronograf/v1/sources/:id/users", service.NewSourceUser)
router.GET("/chronograf/v1/sources/:id/users/:uid", service.SourceUserID)
router.DELETE("/chronograf/v1/sources/:id/users/:uid", service.RemoveSourceUser)
router.PATCH("/chronograf/v1/sources/:id/users/:uid", service.UpdateSourceUser)
// Roles associated with the data source
router.GET("/chronograf/v1/sources/:id/roles", service.Roles)
router.POST("/chronograf/v1/sources/:id/roles", service.NewRole)
router.GET("/chronograf/v1/sources/:id/roles/:rid", service.RoleID)
router.DELETE("/chronograf/v1/sources/:id/roles/:rid", service.RemoveRole)
router.PATCH("/chronograf/v1/sources/:id/roles/:rid", service.UpdateRole)
// Kapacitor
router.GET("/chronograf/v1/sources/:id/kapacitors", service.Kapacitors)
router.POST("/chronograf/v1/sources/:id/kapacitors", service.NewKapacitor)
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid", service.KapacitorsID)
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid", service.UpdateKapacitor)
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid", service.RemoveKapacitor)
// Kapacitor rules
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesGet)
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/rules", service.KapacitorRulesPost)
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesID)
router.PUT("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesPut)
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesStatus)
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesDelete)
// Kapacitor Proxy
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyGet)
router.POST("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPost)
router.PATCH("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyPatch)
router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/proxy", service.KapacitorProxyDelete)
// Mappings
router.GET("/chronograf/v1/mappings", service.GetMappings)
// Layouts
router.GET("/chronograf/v1/layouts", service.Layouts)
router.POST("/chronograf/v1/layouts", service.NewLayout)
router.GET("/chronograf/v1/layouts/:id", service.LayoutsID)
router.PUT("/chronograf/v1/layouts/:id", service.UpdateLayout)
router.DELETE("/chronograf/v1/layouts/:id", service.RemoveLayout)
// Users
router.GET("/chronograf/v1/me", service.Me)
// Dashboards
router.GET("/chronograf/v1/dashboards", service.Dashboards)
router.POST("/chronograf/v1/dashboards", service.NewDashboard)
router.GET("/chronograf/v1/dashboards/:id", service.DashboardID)
router.DELETE("/chronograf/v1/dashboards/:id", service.RemoveDashboard)
router.PUT("/chronograf/v1/dashboards/:id", service.ReplaceDashboard)
router.PATCH("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
var authRoutes AuthRoutes
var out http.Handler
/* Authentication */
if opts.UseAuth {
// Encapsulate the router with OAuth2
var auth http.Handler
auth, authRoutes = AuthAPI(opts, router)
// Create middleware to redirect to the appropriate provider logout
targetURL := "/"
router.GET("/oauth/logout", Logout(targetURL, authRoutes))
out = Logger(opts.Logger, auth)
} else {
out = Logger(opts.Logger, router)
}
router.GET("/chronograf/v1/", AllRoutes(authRoutes, opts.Logger))
router.GET("/chronograf/v1", AllRoutes(authRoutes, opts.Logger))
return out
}
// AuthAPI adds the OAuth routes if auth is enabled.
// TODO: this function is not great. Would be good if providers added their routes.
func AuthAPI(opts MuxOpts, router *httprouter.Router) (http.Handler, AuthRoutes) {
auth := oauth2.NewJWT(opts.TokenSecret)
routes := AuthRoutes{}
for _, pf := range opts.ProviderFuncs {
pf(func(p oauth2.Provider, m oauth2.Mux) {
loginPath := fmt.Sprintf("%s/oauth/%s/login", opts.Basepath, strings.ToLower(p.Name()))
logoutPath := fmt.Sprintf("%s/oauth/%s/logout", opts.Basepath, strings.ToLower(p.Name()))
callbackPath := fmt.Sprintf("%s/oauth/%s/callback", opts.Basepath, strings.ToLower(p.Name()))
router.Handler("GET", loginPath, m.Login())
router.Handler("GET", logoutPath, m.Logout())
router.Handler("GET", callbackPath, m.Callback())
routes = append(routes, AuthRoute{
Name: p.Name(),
Label: strings.Title(p.Name()),
Login: loginPath,
Logout: logoutPath,
Callback: callbackPath,
})
})
}
tokenMiddleware := oauth2.AuthorizedToken(&auth, &oauth2.CookieExtractor{Name: "session"}, opts.Logger, router)
// Wrap the API with token validation middleware.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/chronograf/v1/") || r.URL.Path == "/oauth/logout" {
tokenMiddleware.ServeHTTP(w, r)
return
}
router.ServeHTTP(w, r)
}), routes
}
func encodeJSON(w http.ResponseWriter, status int, v interface{}, logger chronograf.Logger) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(v); err != nil {
unknownErrorWithMessage(w, err, logger)
}
}
// Error writes an JSON message
func Error(w http.ResponseWriter, code int, msg string, logger chronograf.Logger) {
e := ErrorMessage{
Code: code,
Message: msg,
}
b, err := json.Marshal(e)
if err != nil {
code = http.StatusInternalServerError
b = []byte(`{"code": 500, "message":"server_error"}`)
}
logger.
WithField("component", "server").
WithField("http_status ", code).
Error("Error message ", msg)
w.Header().Set("Content-Type", JSONType)
w.WriteHeader(code)
_, _ = w.Write(b)
}
func invalidData(w http.ResponseWriter, err error, logger chronograf.Logger) {
Error(w, http.StatusUnprocessableEntity, fmt.Sprintf("%v", err), logger)
}
func invalidJSON(w http.ResponseWriter, logger chronograf.Logger) {
Error(w, http.StatusBadRequest, "Unparsable JSON", logger)
}
func unknownErrorWithMessage(w http.ResponseWriter, err error, logger chronograf.Logger) {
Error(w, http.StatusInternalServerError, fmt.Sprintf("Unknown error: %v", err), logger)
}
func notFound(w http.ResponseWriter, id int, logger chronograf.Logger) {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), logger)
}
func paramID(key string, r *http.Request) (int, error) {
ctx := r.Context()
param := httprouter.GetParamFromContext(ctx, key)
id, err := strconv.Atoi(param)
if err != nil {
return -1, fmt.Errorf("Error converting ID %s", param)
}
return id, nil
}

View File

@ -0,0 +1,25 @@
// +build OMIT
package http
import (
"io"
"os"
)
func FileServer(root FileSystem) Handler
type FileSystem interface {
Open(name string) (File, error)
}
// A File is returned by a FileSystem's Open method and can be
// served by the FileServer implementation.
//
// The methods should behave the same as those on an *os.File.
type File interface {
io.Closer
io.Reader
io.Seeker
Readdir(count int) ([]os.FileInfo, error)
Stat() (os.FileInfo, error)
}

View File

@ -0,0 +1,51 @@
// +build OMIT
package server
import (
"bufio"
"bytes"
"io"
"net/http"
"github.com/influxdata/chronograf"
)
// URLPrefixer is a wrapper for an http.Handler that will prefix all occurrences of a relative URL with the configured Prefix
type URLPrefixer struct {
Prefix string // the prefix to be appended after any detected Attrs
Next http.Handler // the http.Handler which will generate the content to be modified by this handler
Attrs [][]byte // a list of attrs that should have their URLs prefixed. For example `src="` or `href="` would be valid
}
func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
...
// Read next handler's response byte by byte
src := bufio.NewScanner(nextRead)
src.Split(bufio.ScanBytes)
for {
window := buf.Bytes()
if matchlen, match := up.match(window, up.Attrs...); matchlen != 0 {
buf.Next(matchlen) // advance to the relative URL
for i := 0; i < matchlen; i++ {
src.Scan()
buf.Write(src.Bytes())
}
rw.Write(match) // add the src attr to the output
io.WriteString(rw, up.Prefix) // write the prefix
} else {...}
}
}
func NewDefaultURLPrefixer(prefix string, next http.Handler) *URLPrefixer {
return &URLPrefixer{
Prefix: prefix,
Next: next,
Logger: lg,
Attrs: [][]byte{
[]byte(`src="`),
[]byte(`href="`),
[]byte(`url(`),
[]byte(`data-basepath="`), // for forwarding basepath to frontend
},
}
}

View File

@ -0,0 +1,24 @@
// +build OMIT
import (
"net/http"
"os"
)
type Dir struct {
Default string
dir http.Dir
}
func (d Dir) Open(name string) (http.File, error) {
f, err := d.dir.Open(name)
if err != nil {
f, err = os.Open(d.Default)
if err != nil {
return nil, err
}
return f, nil
}
return f, err
}
// OMIT END

View File

@ -0,0 +1,14 @@
// +build OMIT
package main
import (
"log"
"net/http"
)
func main() {
log.Fatal(http.ListenAndServe(":8888", http.FileServer(&Dir{
Default: "src/ui/index.html",
dir: http.Dir("src/ui"),
})))
}

View File

@ -0,0 +1,11 @@
// +build OMIT
package main
import (
"log"
"net/http"
)
func main() {
log.Fatal(http.ListenAndServe(":8888", http.FileServer(http.Dir("ui/build"))))
}

View File

@ -0,0 +1,15 @@
// +build OMIT
package server
import (
"net/http"
)
// Version handler adds X-Chronograf-Version header to responses
func Version(version string, h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("X-Chronograf-Version", version)
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}