Add talk for Minneapolis Ultimate Go
parent
733125b480
commit
295388cd87
|
@ -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 .
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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"),
|
||||
})))
|
||||
}
|
|
@ -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"))))
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue