Merge pull request #814 from influxdata/feature/tr-host-under-path
Enable Chronograf to be hosted under any arbitrary pathpull/10616/head
commit
745ad17da7
|
@ -2,13 +2,14 @@
|
|||
|
||||
### Upcoming Bug Fixes
|
||||
1. [#788](https://github.com/influxdata/chronograf/pull/788): Fix missing fields in data explorer when using non-default retention policy
|
||||
1. [#774](https://github.com/influxdata/chronograf/issues/774): Fix gaps in layouts for hosts
|
||||
2. [#774](https://github.com/influxdata/chronograf/issues/774): Fix gaps in layouts for hosts
|
||||
|
||||
### Upcoming Features
|
||||
1. [#779](https://github.com/influxdata/chronograf/issues/779): Add layout for telegraf's diskio system plugin
|
||||
2. [#810](https://github.com/influxdata/chronograf/issues/810): Add layout for telegraf's net system plugin
|
||||
3. [#811](https://github.com/influxdata/chronograf/issues/811): Add layout for telegraf's procstat plugin
|
||||
3. [#737](https://github.com/influxdata/chronograf/issues/737): Add GUI for OpsGenie kapacitor alert service
|
||||
4. [#737](https://github.com/influxdata/chronograf/issues/737): Add GUI for OpsGenie kapacitor alert service
|
||||
5. [#814](https://github.com/influxdata/chronograf/issues/814): Allows Chronograf to be mounted under any arbitrary URL path using the `--basepath` flag.
|
||||
|
||||
### Upcoming UI Improvements
|
||||
|
||||
|
|
|
@ -37,10 +37,14 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
|
|||
Develop: opts.Develop,
|
||||
Logger: opts.Logger,
|
||||
})
|
||||
|
||||
// Prefix any URLs found in the React assets with any configured basepath
|
||||
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger)
|
||||
|
||||
// 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 = assets
|
||||
router.NotFound = prefixedAssets
|
||||
|
||||
/* Documentation */
|
||||
router.GET("/swagger.json", Spec())
|
||||
|
|
|
@ -19,7 +19,10 @@ import (
|
|||
"github.com/tylerb/graceful"
|
||||
)
|
||||
|
||||
var startTime time.Time
|
||||
var (
|
||||
startTime time.Time
|
||||
basepath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
startTime = time.Now().UTC()
|
||||
|
@ -47,6 +50,7 @@ type Server struct {
|
|||
ReportingDisabled bool `short:"r" long:"reporting-disabled" description:"Disable reporting of usage stats (os,arch,version,cluster_id,uptime) once every 24hr" env:"REPORTING_DISABLED"`
|
||||
LogLevel string `short:"l" long:"log-level" value-name:"choice" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" default:"info" description:"Set the logging level" env:"LOG_LEVEL"`
|
||||
ShowVersion bool `short:"v" long:"version" description:"Show Chronograf version info"`
|
||||
Basepath string `long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted"`
|
||||
BuildInfo BuildInfo
|
||||
Listener net.Listener
|
||||
handler http.Handler
|
||||
|
@ -66,6 +70,7 @@ func (s *Server) useAuth() bool {
|
|||
func (s *Server) Serve() error {
|
||||
logger := clog.New(clog.ParseLevel(s.LogLevel))
|
||||
service := openService(s.BoltPath, s.CannedPath, logger, s.useAuth())
|
||||
basepath = s.Basepath
|
||||
s.handler = NewMux(MuxOpts{
|
||||
Develop: s.Develop,
|
||||
TokenSecret: s.TokenSecret,
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
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
|
||||
Logger chronograf.Logger // The logger where prefixing errors will be dispatched to
|
||||
}
|
||||
|
||||
type wrapResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
Substitute *io.PipeWriter
|
||||
|
||||
headerWritten bool
|
||||
dupHeader http.Header
|
||||
}
|
||||
|
||||
func (wrw wrapResponseWriter) Write(p []byte) (int, error) {
|
||||
return wrw.Substitute.Write(p)
|
||||
}
|
||||
|
||||
func (wrw wrapResponseWriter) WriteHeader(code int) {
|
||||
if !wrw.headerWritten {
|
||||
wrw.ResponseWriter.Header().Set("Content-Type", wrw.Header().Get("Content-Type"))
|
||||
wrw.headerWritten = true
|
||||
}
|
||||
wrw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// Header() copies the Header map from the underlying ResponseWriter to prevent
|
||||
// modifications to it by callers
|
||||
func (wrw wrapResponseWriter) Header() http.Header {
|
||||
wrw.dupHeader = http.Header{}
|
||||
origHeader := wrw.ResponseWriter.Header()
|
||||
for k, v := range origHeader {
|
||||
wrw.dupHeader[k] = v
|
||||
}
|
||||
return wrw.dupHeader
|
||||
}
|
||||
|
||||
const CHUNK_SIZE int = 512
|
||||
|
||||
// ServeHTTP implements an http.Handler that prefixes relative URLs from the
|
||||
// Next handler with the configured prefix. It does this by examining the
|
||||
// stream through the ResponseWriter, and appending the Prefix after any of the
|
||||
// Attrs detected in the stream.
|
||||
func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
// chunked transfer because we're modifying the response on the fly, so we
|
||||
// won't know the final content-length
|
||||
rw.Header().Set("Connection", "Keep-Alive")
|
||||
rw.Header().Set("Transfer-Encoding", "chunked")
|
||||
|
||||
writtenCount := 0 // number of bytes written to rw
|
||||
|
||||
// extract the flusher for flushing chunks
|
||||
flusher, ok := rw.(http.Flusher)
|
||||
if !ok {
|
||||
up.Logger.
|
||||
WithField("component", "prefixer").
|
||||
Fatal("Expected http.ResponseWriter to be an http.Flusher, but wasn't")
|
||||
}
|
||||
|
||||
nextRead, nextWrite := io.Pipe()
|
||||
go func() {
|
||||
defer nextWrite.Close()
|
||||
up.Next.ServeHTTP(wrapResponseWriter{ResponseWriter: rw, Substitute: nextWrite}, r)
|
||||
}()
|
||||
|
||||
// setup a buffer which is the max length of our target attrs
|
||||
b := make([]byte, up.maxlen(up.Attrs...))
|
||||
io.ReadFull(nextRead, b) // prime the buffer with the start of the input
|
||||
buf := bytes.NewBuffer(b)
|
||||
|
||||
// Read next handler's response byte by byte
|
||||
src := bufio.NewScanner(nextRead)
|
||||
src.Split(bufio.ScanBytes)
|
||||
for {
|
||||
window := buf.Bytes()
|
||||
|
||||
// advance a byte if window is not a src attr
|
||||
if matchlen, match := up.match(window, up.Attrs...); matchlen == 0 {
|
||||
if src.Scan() {
|
||||
// shift the next byte into buf
|
||||
rw.Write(buf.Next(1))
|
||||
writtenCount++
|
||||
buf.Write(src.Bytes())
|
||||
|
||||
if writtenCount >= CHUNK_SIZE {
|
||||
flusher.Flush()
|
||||
writtenCount = 0
|
||||
}
|
||||
} else {
|
||||
if err := src.Err(); err != nil {
|
||||
up.Logger.
|
||||
WithField("component", "prefixer").
|
||||
Error("Error encountered while scanning: err:", err)
|
||||
}
|
||||
rw.Write(window)
|
||||
flusher.Flush()
|
||||
break
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// match compares the subject against a list of targets. If there is a match
|
||||
// between any of them a non-zero value is returned. The returned value is the
|
||||
// length of the match. It is assumed that subject's length > length of all
|
||||
// targets. The matching []byte is also returned as the second return parameter
|
||||
func (up *URLPrefixer) match(subject []byte, targets ...[]byte) (int, []byte) {
|
||||
for _, target := range targets {
|
||||
if bytes.Equal(subject[:len(target)], target) {
|
||||
return len(target), target
|
||||
}
|
||||
}
|
||||
return 0, []byte{}
|
||||
}
|
||||
|
||||
// maxlen returns the length of the largest []byte provided to it as an argument
|
||||
func (up *URLPrefixer) maxlen(targets ...[]byte) int {
|
||||
max := 0
|
||||
for _, tgt := range targets {
|
||||
if tlen := len(tgt); tlen > max {
|
||||
max = tlen
|
||||
}
|
||||
}
|
||||
return max
|
||||
}
|
||||
|
||||
// NewDefaultURLPrefixer returns a URLPrefixer that will prefix any src and
|
||||
// href attributes found in HTML as well as any url() directives found in CSS
|
||||
// with the provided prefix. Additionally, it will prefix any `data-basepath`
|
||||
// attributes as well for informing front end logic about any prefixes. `next`
|
||||
// is the next http.Handler that will have its output prefixed
|
||||
func NewDefaultURLPrefixer(prefix string, next http.Handler, lg chronograf.Logger) *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,108 @@
|
|||
package server_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/influxdata/chronograf/server"
|
||||
)
|
||||
|
||||
var prefixerTests = []struct {
|
||||
name string
|
||||
subject string
|
||||
expected string
|
||||
shouldErr bool
|
||||
attrs [][]byte
|
||||
}{
|
||||
{
|
||||
`One script tag`,
|
||||
`<script type="text/javascript" src="/loljavascript.min.js">`,
|
||||
`<script type="text/javascript" src="/arbitraryprefix/loljavascript.min.js">`,
|
||||
false,
|
||||
[][]byte{
|
||||
[]byte(`src="`),
|
||||
},
|
||||
},
|
||||
{
|
||||
`Two script tags`,
|
||||
`<script type="text/javascript" src="/loljavascript.min.js"><script type="text/javascript" src="/anotherscript.min.js">`,
|
||||
`<script type="text/javascript" src="/arbitraryprefix/loljavascript.min.js"><script type="text/javascript" src="/arbitraryprefix/anotherscript.min.js">`,
|
||||
false,
|
||||
[][]byte{
|
||||
[]byte(`src="`),
|
||||
},
|
||||
},
|
||||
{
|
||||
`Link href`,
|
||||
`<link rel="shortcut icon" href="/favicon.ico">`,
|
||||
`<link rel="shortcut icon" href="/arbitraryprefix/favicon.ico">`,
|
||||
false,
|
||||
[][]byte{
|
||||
[]byte(`src="`),
|
||||
[]byte(`href="`),
|
||||
},
|
||||
},
|
||||
{
|
||||
`Trailing HTML`,
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title>Chronograf</title>
|
||||
<link rel="shortcut icon" href="/favicon.ico"><link href="/chronograf.css" rel="stylesheet"></head>
|
||||
<body>
|
||||
<div id='react-root'></div>
|
||||
<script type="text/javascript" src="/manifest.7489452b099f9581ca1b.dev.js"></script><script type="text/javascript" src="/vendor.568c0101d870a13ecff9.dev.js"></script><script type="text/javascript" src="/app.13d0ce0b33609be3802b.dev.js"></script></body>
|
||||
</html>`,
|
||||
`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<title>Chronograf</title>
|
||||
<link rel="shortcut icon" href="/arbitraryprefix/favicon.ico"><link href="/arbitraryprefix/chronograf.css" rel="stylesheet"></head>
|
||||
<body>
|
||||
<div id='react-root'></div>
|
||||
<script type="text/javascript" src="/arbitraryprefix/manifest.7489452b099f9581ca1b.dev.js"></script><script type="text/javascript" src="/arbitraryprefix/vendor.568c0101d870a13ecff9.dev.js"></script><script type="text/javascript" src="/arbitraryprefix/app.13d0ce0b33609be3802b.dev.js"></script></body>
|
||||
</html>`,
|
||||
false,
|
||||
[][]byte{
|
||||
[]byte(`src="`),
|
||||
[]byte(`href="`),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_Server_Prefixer_RewritesURLs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range prefixerTests {
|
||||
subject := test.subject
|
||||
expected := test.expected
|
||||
|
||||
backend := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, subject)
|
||||
})
|
||||
|
||||
pfx := &server.URLPrefixer{Prefix: "/arbitraryprefix", Next: backend, Attrs: test.attrs}
|
||||
|
||||
ts := httptest.NewServer(pfx)
|
||||
defer ts.Close()
|
||||
|
||||
res, err := http.Get(ts.URL)
|
||||
if err != nil {
|
||||
t.Error("Unexpected error fetching from prefixer: err:", err)
|
||||
}
|
||||
|
||||
actual, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Error("Unable to read prefixed body: err:", err)
|
||||
}
|
||||
|
||||
if string(actual) != expected+"\n" {
|
||||
t.Error(test.name, ":\n Unsuccessful prefixing.\n\tWant:", fmt.Sprintf("%+q", expected), "\n\tGot: ", fmt.Sprintf("%+q", string(actual)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import {render} from 'react-dom';
|
||||
import {Provider} from 'react-redux';
|
||||
import {Router, Route, browserHistory, Redirect} from 'react-router';
|
||||
import {Router, Route, Redirect} from 'react-router';
|
||||
import {createHistory, useBasename} from 'history';
|
||||
|
||||
import App from 'src/App';
|
||||
import AlertsApp from 'src/alerts';
|
||||
|
@ -28,6 +29,19 @@ const timeRange = Object.assign(defaultTimeRange, parsedTimeRange);
|
|||
const store = configureStore({timeRange});
|
||||
const rootNode = document.getElementById('react-root');
|
||||
|
||||
let browserHistory;
|
||||
const basepath = rootNode.dataset.basepath;
|
||||
window.basepath = basepath;
|
||||
if (basepath) {
|
||||
browserHistory = useBasename(createHistory)({
|
||||
basename: basepath, // this is written in when available by the URL prefixer middleware
|
||||
});
|
||||
} else {
|
||||
browserHistory = useBasename(createHistory)({
|
||||
basename: "",
|
||||
});
|
||||
}
|
||||
|
||||
const Root = React.createClass({
|
||||
getInitialState() {
|
||||
return {
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
<title>Chronograf</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id='react-root'></div>
|
||||
<div id='react-root' data-basepath=""></div>
|
||||
</body>
|
||||
</html
|
||||
</html>
|
||||
|
|
|
@ -7,6 +7,9 @@ export default function AJAX({
|
|||
params = {},
|
||||
headers = {},
|
||||
}) {
|
||||
if (window.basepath) {
|
||||
url = `${window.basepath}${url}`;
|
||||
}
|
||||
return axios({
|
||||
url,
|
||||
method,
|
||||
|
|
Loading…
Reference in New Issue