Merge branch 'master' of github.com:influxdata/chronograf

pull/2158/head^2
Andrew Watkins 2017-10-27 09:10:35 -07:00
commit a60b458dd6
16 changed files with 269 additions and 175 deletions

View File

@ -559,13 +559,14 @@ type KapacitorProperty struct {
// Server represents a proxy connection to an HTTP server
type Server struct {
ID int // ID is the unique ID of the server
SrcID int // SrcID of the data source
Name string // Name is the user-defined name for the server
Username string // Username is the username to connect to the server
Password string // Password is in CLEARTEXT
URL string // URL are the connections to the server
Active bool // Is this the active server for the source?
ID int // ID is the unique ID of the server
SrcID int // SrcID of the data source
Name string // Name is the user-defined name for the server
Username string // Username is the username to connect to the server
Password string // Password is in CLEARTEXT
URL string // URL are the connections to the server
InsecureSkipVerify bool // InsecureSkipVerify as true means any certificate presented by the server is accepted.
Active bool // Is this the active server for the source?
}
// ServersStore stores connection information for a `Server`

View File

@ -3,7 +3,7 @@ machine:
services:
- docker
environment:
DOCKER_TAG: chronograf-20170516
DOCKER_TAG: chronograf-20171027
dependencies:
override:

View File

@ -18,7 +18,7 @@ RUN pip install boto requests python-jose --upgrade
RUN gem install fpm
# Install node
ENV NODE_VERSION v6.10.3
ENV NODE_VERSION v6.11.5
RUN wget -q https://nodejs.org/dist/latest-v6.x/node-${NODE_VERSION}-linux-x64.tar.gz; \
tar -xvf node-${NODE_VERSION}-linux-x64.tar.gz -C / --strip-components=1; \
rm -f node-${NODE_VERSION}-linux-x64.tar.gz
@ -35,7 +35,7 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
# Install go
ENV GOPATH /root/go
ENV GO_VERSION 1.8.1
ENV GO_VERSION 1.9.2
ENV GO_ARCH amd64
RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz; \
tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz ; \

View File

@ -19,12 +19,13 @@ const (
// Client communicates to kapacitor
type Client struct {
URL string
Username string
Password string
ID chronograf.ID
Ticker chronograf.Ticker
kapaClient func(url, username, password string) (KapaClient, error)
URL string
Username string
Password string
InsecureSkipVerify bool
ID chronograf.ID
Ticker chronograf.Ticker
kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error)
}
// KapaClient represents a connection to a kapacitor instance
@ -37,14 +38,15 @@ type KapaClient interface {
}
// NewClient creates a client that interfaces with Kapacitor tasks
func NewClient(url, username, password string) *Client {
func NewClient(url, username, password string, insecureSkipVerify bool) *Client {
return &Client{
URL: url,
Username: username,
Password: password,
ID: &uuid.V4{},
Ticker: &Alert{},
kapaClient: NewKapaClient,
URL: url,
Username: username,
Password: password,
InsecureSkipVerify: insecureSkipVerify,
ID: &uuid.V4{},
Ticker: &Alert{},
kapaClient: NewKapaClient,
}
}
@ -121,7 +123,7 @@ func (c *Client) Create(ctx context.Context, rule chronograf.AlertRule) (*Task,
return nil, err
}
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify)
if err != nil {
return nil, err
}
@ -189,7 +191,7 @@ func (c *Client) createFromQueryConfig(rule chronograf.AlertRule) (*client.Creat
// Delete removes tickscript task from kapacitor
func (c *Client) Delete(ctx context.Context, href string) error {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify)
if err != nil {
return err
}
@ -197,7 +199,7 @@ func (c *Client) Delete(ctx context.Context, href string) error {
}
func (c *Client) updateStatus(ctx context.Context, href string, status client.TaskStatus) (*Task, error) {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify)
if err != nil {
return nil, err
}
@ -235,7 +237,7 @@ func (c *Client) Status(ctx context.Context, href string) (string, error) {
}
func (c *Client) status(ctx context.Context, href string) (client.TaskStatus, error) {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify)
if err != nil {
return 0, err
}
@ -249,7 +251,7 @@ func (c *Client) status(ctx context.Context, href string) (client.TaskStatus, er
// All returns all tasks in kapacitor
func (c *Client) All(ctx context.Context) (map[string]*Task, error) {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify)
if err != nil {
return nil, err
}
@ -286,7 +288,7 @@ func (c *Client) Reverse(id string, script chronograf.TICKScript) chronograf.Ale
// Get returns a single alert in kapacitor
func (c *Client) Get(ctx context.Context, id string) (*Task, error) {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify)
if err != nil {
return nil, err
}
@ -301,7 +303,7 @@ func (c *Client) Get(ctx context.Context, id string) (*Task, error) {
// Update changes the tickscript of a given id.
func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertRule) (*Task, error) {
kapa, err := c.kapaClient(c.URL, c.Username, c.Password)
kapa, err := c.kapaClient(c.URL, c.Username, c.Password, c.InsecureSkipVerify)
if err != nil {
return nil, err
}
@ -386,7 +388,7 @@ func toTask(q *chronograf.QueryConfig) client.TaskType {
}
// NewKapaClient creates a Kapacitor client connection
func NewKapaClient(url, username, password string) (KapaClient, error) {
func NewKapaClient(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
var creds *client.Credentials
if username != "" {
creds = &client.Credentials{
@ -397,8 +399,9 @@ func NewKapaClient(url, username, password string) (KapaClient, error) {
}
clnt, err := client.New(client.Config{
URL: url,
Credentials: creds,
URL: url,
Credentials: creds,
InsecureSkipVerify: insecureSkipVerify,
})
if err != nil {

View File

@ -75,7 +75,7 @@ func TestClient_All(t *testing.T) {
Password string
ID chronograf.ID
Ticker chronograf.Ticker
kapaClient func(url, username, password string) (KapaClient, error)
kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error)
}
type args struct {
ctx context.Context
@ -100,7 +100,7 @@ func TestClient_All(t *testing.T) {
{
name: "return no tasks",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
},
@ -110,7 +110,7 @@ func TestClient_All(t *testing.T) {
{
name: "return a non-reversible task",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
},
@ -141,7 +141,7 @@ func TestClient_All(t *testing.T) {
{
name: "return a reversible task",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
},
@ -380,7 +380,7 @@ func TestClient_Get(t *testing.T) {
Password string
ID chronograf.ID
Ticker chronograf.Ticker
kapaClient func(url, username, password string) (KapaClient, error)
kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error)
}
type args struct {
ctx context.Context
@ -406,7 +406,7 @@ func TestClient_Get(t *testing.T) {
{
name: "return no task",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
},
@ -423,7 +423,7 @@ func TestClient_Get(t *testing.T) {
{
name: "return non-reversible task",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
},
@ -465,7 +465,7 @@ func TestClient_Get(t *testing.T) {
{
name: "return reversible task",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
},
@ -706,7 +706,7 @@ func TestClient_updateStatus(t *testing.T) {
Password string
ID chronograf.ID
Ticker chronograf.Ticker
kapaClient func(url, username, password string) (KapaClient, error)
kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error)
}
type args struct {
ctx context.Context
@ -727,7 +727,7 @@ func TestClient_updateStatus(t *testing.T) {
{
name: "disable alert rule",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
@ -777,7 +777,7 @@ func TestClient_updateStatus(t *testing.T) {
{
name: "fail to enable alert rule",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
@ -797,7 +797,7 @@ func TestClient_updateStatus(t *testing.T) {
{
name: "enable alert rule",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
@ -880,7 +880,7 @@ func TestClient_Update(t *testing.T) {
Password string
ID chronograf.ID
Ticker chronograf.Ticker
kapaClient func(url, username, password string) (KapaClient, error)
kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error)
}
type args struct {
ctx context.Context
@ -902,7 +902,7 @@ func TestClient_Update(t *testing.T) {
{
name: "update alert rule error",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
@ -936,7 +936,7 @@ func TestClient_Update(t *testing.T) {
{
name: "update alert rule",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
@ -1000,7 +1000,7 @@ func TestClient_Update(t *testing.T) {
{
name: "stays disabled when already disabled",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
@ -1099,7 +1099,7 @@ func TestClient_Create(t *testing.T) {
Password string
ID chronograf.ID
Ticker chronograf.Ticker
kapaClient func(url, username, password string) (KapaClient, error)
kapaClient func(url, username, password string, insecureSkipVerify bool) (KapaClient, error)
}
type args struct {
ctx context.Context
@ -1119,7 +1119,7 @@ func TestClient_Create(t *testing.T) {
{
name: "create alert rule",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},
@ -1185,7 +1185,7 @@ func TestClient_Create(t *testing.T) {
{
name: "create alert rule error",
fields: fields{
kapaClient: func(url, username, password string) (KapaClient, error) {
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
return kapa, nil
},
Ticker: &Alert{},

View File

@ -12,11 +12,12 @@ import (
)
type postKapacitorRequest struct {
Name *string `json:"name"` // User facing name of kapacitor instance.; Required: true
URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
Active bool `json:"active"`
Name *string `json:"name"` // User facing name of kapacitor instance.; Required: true
URL *string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092);/ Required: true
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
Active bool `json:"active"`
}
func (p *postKapacitorRequest) Valid() error {
@ -44,13 +45,14 @@ type kapaLinks struct {
}
type kapacitor struct {
ID int `json:"id,string"` // Unique identifier representing a kapacitor instance.
Name string `json:"name"` // User facing name of kapacitor instance.
URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092)
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
Active bool `json:"active"`
Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor
ID int `json:"id,string"` // Unique identifier representing a kapacitor instance.
Name string `json:"name"` // User facing name of kapacitor instance.
URL string `json:"url"` // URL for the kapacitor backend (e.g. http://localhost:9092)
Username string `json:"username,omitempty"` // Username for authentication to kapacitor
Password string `json:"password,omitempty"`
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
Active bool `json:"active"`
Links kapaLinks `json:"links"` // Links are URI locations related to kapacitor
}
// NewKapacitor adds valid kapacitor store store.
@ -79,12 +81,13 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
}
srv := chronograf.Server{
SrcID: srcID,
Name: *req.Name,
Username: req.Username,
Password: req.Password,
URL: *req.URL,
Active: req.Active,
SrcID: srcID,
Name: *req.Name,
Username: req.Username,
Password: req.Password,
InsecureSkipVerify: req.InsecureSkipVerify,
URL: *req.URL,
Active: req.Active,
}
if srv, err = h.ServersStore.Add(ctx, srv); err != nil {
@ -101,11 +104,12 @@ func (h *Service) NewKapacitor(w http.ResponseWriter, r *http.Request) {
func newKapacitor(srv chronograf.Server) kapacitor {
httpAPISrcs := "/chronograf/v1/sources"
return kapacitor{
ID: srv.ID,
Name: srv.Name,
Username: srv.Username,
URL: srv.URL,
Active: srv.Active,
ID: srv.ID,
Name: srv.Name,
Username: srv.Username,
URL: srv.URL,
Active: srv.Active,
InsecureSkipVerify: srv.InsecureSkipVerify,
Links: kapaLinks{
Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID),
Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID),
@ -204,11 +208,12 @@ func (h *Service) RemoveKapacitor(w http.ResponseWriter, r *http.Request) {
}
type patchKapacitorRequest struct {
Name *string `json:"name,omitempty"` // User facing name of kapacitor instance.
URL *string `json:"url,omitempty"` // URL for the kapacitor
Username *string `json:"username,omitempty"` // Username for kapacitor auth
Password *string `json:"password,omitempty"`
Active *bool `json:"active"`
Name *string `json:"name,omitempty"` // User facing name of kapacitor instance.
URL *string `json:"url,omitempty"` // URL for the kapacitor
Username *string `json:"username,omitempty"` // Username for kapacitor auth
Password *string `json:"password,omitempty"`
InsecureSkipVerify *bool `json:"insecureSkipVerify,omitempty"` // InsecureSkipVerify as true means any certificate presented by the kapacitor is accepted.
Active *bool `json:"active"`
}
func (p *patchKapacitorRequest) Valid() error {
@ -268,6 +273,9 @@ func (h *Service) UpdateKapacitor(w http.ResponseWriter, r *http.Request) {
if req.Username != nil {
srv.Username = *req.Username
}
if req.InsecureSkipVerify != nil {
srv.InsecureSkipVerify = *req.InsecureSkipVerify
}
if req.Active != nil {
srv.Active = *req.Active
}
@ -303,7 +311,7 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
return
}
c := kapa.NewClient(srv.URL, srv.Username, srv.Password)
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -433,7 +441,7 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
}
tid := httprouter.GetParamFromContext(ctx, "tid")
c := kapa.NewClient(srv.URL, srv.Username, srv.Password)
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
var req chronograf.AlertRule
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
@ -503,7 +511,7 @@ func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
}
tid := httprouter.GetParamFromContext(ctx, "tid")
c := kapa.NewClient(srv.URL, srv.Username, srv.Password)
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
var req KapacitorStatus
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -563,7 +571,7 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
return
}
c := kapa.NewClient(srv.URL, srv.Username, srv.Password)
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
tasks, err := c.All(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
@ -606,7 +614,7 @@ func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
}
tid := httprouter.GetParamFromContext(ctx, "tid")
c := kapa.NewClient(srv.URL, srv.Username, srv.Password)
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
// Check if the rule exists within scope
task, err := c.Get(ctx, tid)
@ -644,7 +652,7 @@ func (h *Service) KapacitorRulesDelete(w http.ResponseWriter, r *http.Request) {
return
}
c := kapa.NewClient(srv.URL, srv.Username, srv.Password)
c := kapa.NewClient(srv.URL, srv.Username, srv.Password, srv.InsecureSkipVerify)
tid := httprouter.GetParamFromContext(ctx, "tid")
// Check if the rule is linked to this server and kapacitor

View File

@ -186,6 +186,14 @@ func Test_KapacitorRulesGet(t *testing.T) {
// setup mock service and test logger
testLogger := mocks.TestLogger{}
svc := &server.Service{
SourcesStore: &mocks.SourcesStore{
GetF: func(ctx context.Context, ID int) (chronograf.Source, error) {
return chronograf.Source{
ID: ID,
InsecureSkipVerify: true,
}, nil
},
},
ServersStore: &mocks.ServersStore{
GetF: func(ctx context.Context, ID int) (chronograf.Server, error) {
return chronograf.Server{

View File

@ -7,39 +7,57 @@ import (
"github.com/influxdata/chronograf"
)
type logResponseWriter struct {
// statusWriterFlusher captures the status header of an http.ResponseWriter
// and is a flusher
type statusWriter struct {
http.ResponseWriter
responseCode int
Flusher http.Flusher
status int
}
func (l *logResponseWriter) WriteHeader(status int) {
l.responseCode = status
l.ResponseWriter.WriteHeader(status)
func (w *statusWriter) WriteHeader(status int) {
w.status = status
w.ResponseWriter.WriteHeader(status)
}
func (w *statusWriter) Status() int { return w.status }
// Flush is here because the underlying HTTP chunked transfer response writer
// to implement http.Flusher. Without it data is silently buffered. This
// was discovered when proxying kapacitor chunked logs.
func (w *statusWriter) Flush() {
if w.Flusher != nil {
w.Flusher.Flush()
}
}
// Logger is middleware that logs the request
func Logger(logger chronograf.Logger, next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
logger.
WithField("component", "server").
logger.WithField("component", "server").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("url", r.URL).
Info("Request")
Debug("Request")
lrr := &logResponseWriter{w, 0}
next.ServeHTTP(lrr, r)
sw := &statusWriter{
ResponseWriter: w,
}
if f, ok := w.(http.Flusher); ok {
sw.Flusher = f
}
next.ServeHTTP(sw, r)
later := time.Now()
elapsed := later.Sub(now)
logger.
WithField("component", "server").
WithField("remote_addr", r.RemoteAddr).
WithField("method", r.Method).
WithField("response_time", elapsed.String()).
WithField("code", lrr.responseCode).
Info("Response: ", http.StatusText(lrr.responseCode))
WithField("status", sw.Status()).
Info("Response: ", http.StatusText(sw.Status()))
}
return http.HandlerFunc(fn)
}

View File

@ -9,7 +9,8 @@ import (
type interceptingResponseWriter struct {
http.ResponseWriter
Prefix string
Flusher http.Flusher
Prefix string
}
func (i *interceptingResponseWriter) WriteHeader(status int) {
@ -25,11 +26,26 @@ func (i *interceptingResponseWriter) WriteHeader(status int) {
i.ResponseWriter.WriteHeader(status)
}
// PrefixingRedirector alters the Location header of downstream http.Handlers
// Flush is here because the underlying HTTP chunked transfer response writer
// to implement http.Flusher. Without it data is silently buffered. This
// was discovered when proxying kapacitor chunked logs.
func (i *interceptingResponseWriter) Flush() {
if i.Flusher != nil {
i.Flusher.Flush()
}
}
// PrefixedRedirect alters the Location header of downstream http.Handlers
// to include a specified prefix
func PrefixedRedirect(prefix string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
iw := &interceptingResponseWriter{w, prefix}
iw := &interceptingResponseWriter{
ResponseWriter: w,
Prefix: prefix,
}
if flusher, ok := w.(http.Flusher); ok {
iw.Flusher = flusher
}
next.ServeHTTP(iw, r)
})
}

View File

@ -5,6 +5,8 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
// KapacitorProxy proxies requests to kapacitor using the path query parameter.
@ -34,28 +36,33 @@ func (h *Service) KapacitorProxy(w http.ResponseWriter, r *http.Request) {
return
}
u, err := url.Parse(srv.URL)
// To preserve any HTTP query arguments to the kapacitor path,
// we concat and parse them into u.
uri := singleJoiningSlash(srv.URL, path)
u, err := url.Parse(uri)
if err != nil {
msg := fmt.Sprintf("Error parsing kapacitor url: %v", err)
Error(w, http.StatusUnprocessableEntity, msg, h.Logger)
return
}
u.Path = path
director := func(req *http.Request) {
// Set the Host header of the original Kapacitor URL
req.Host = u.Host
req.URL = u
// Because we are acting as a proxy, kapacitor needs to have the basic auth information set as
// a header directly
if srv.Username != "" && srv.Password != "" {
req.SetBasicAuth(srv.Username, srv.Password)
}
}
// Without a FlushInterval the HTTP Chunked response for kapacitor logs is
// buffered and flushed every 30 seconds.
proxy := &httputil.ReverseProxy{
Director: director,
Director: director,
FlushInterval: time.Second,
}
proxy.ServeHTTP(w, r)
}
@ -79,3 +86,15 @@ func (h *Service) KapacitorProxyGet(w http.ResponseWriter, r *http.Request) {
func (h *Service) KapacitorProxyDelete(w http.ResponseWriter, r *http.Request) {
h.KapacitorProxy(w, r)
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
if aslash && bslash {
return a + b[1:]
}
if !aslash && !bslash {
return a + "/" + b
}
return a + b
}

View File

@ -2357,6 +2357,7 @@
"name": "kapa",
"url": "http://localhost:9092",
"active": false,
"insecureSkipVerify": false,
"links": {
"proxy": "/chronograf/v1/sources/4/kapacitors/4/proxy",
"self": "/chronograf/v1/sources/4/kapacitors/4",
@ -2387,6 +2388,11 @@
"description":
"URL for the kapacitor backend (e.g. http://localhost:9092)"
},
"insecureSkipVerify": {
"type": "boolean",
"description":
"True means any certificate presented by the kapacitor is accepted. Typically used for self-signed certs. Probably should only be used for testing."
},
"active": {
"type": "boolean",
"description":

View File

@ -283,11 +283,13 @@ Dygraph.prototype.findClosestPoint = function(domX, domY) {
minYDist = ydist
closestRow = point.idx
closestSeries = setIdx
closestPoint = point
} else if (xdist === minXDist && ydist < minYDist) {
minXDist = xdist
minYDist = ydist
closestRow = point.idx
closestSeries = setIdx
closestPoint = point
}
}
}

View File

@ -97,7 +97,7 @@ const DatabaseList = React.createClass({
return (
<div className="query-builder--column query-builder--column-db">
<div className="query-builder--heading">Databases</div>
<div className="query-builder--heading">DB.RetentionPolicy</div>
<div className="query-builder--list">
<FancyScrollbar>
{sortedNamespaces.map(namespace => {

View File

@ -29,6 +29,7 @@ export default class Dygraph extends Component {
x: null,
series: [],
},
pageX: null,
sortType: '',
filterText: '',
isSynced: false,
@ -36,7 +37,6 @@ export default class Dygraph extends Component {
isAscending: true,
isSnipped: false,
isFilterVisible: false,
legendArrowPosition: 'top',
}
}
@ -171,9 +171,8 @@ export default class Dygraph extends Component {
dygraph.updateOptions(updateOptions)
const {w} = this.dygraph.getArea()
this.resize()
this.dygraph.resize()
this.props.setResolution(w)
this.resize()
}
handleZoom = (lower, upper) => {
@ -298,6 +297,7 @@ export default class Dygraph extends Component {
resize = () => {
this.dygraph.resizeElements_()
this.dygraph.predraw_()
this.dygraph.resize()
}
formatTimeRange = timeRange => {
@ -341,64 +341,8 @@ export default class Dygraph extends Component {
}
}
highlightCallback = e => {
const chronografChromeSize = 60 // Width & Height of navigation page elements
// Move the Legend on hover
const graphRect = this.graphRef.getBoundingClientRect()
const legendRect = this.legendRef.getBoundingClientRect()
const graphWidth = graphRect.width + 32 // Factoring in padding from parent
const graphHeight = graphRect.height
const graphBottom = graphRect.bottom
const legendWidth = legendRect.width
const legendHeight = legendRect.height
const screenHeight = window.innerHeight
const legendMaxLeft = graphWidth - legendWidth / 2
const trueGraphX = e.pageX - graphRect.left
let legendLeft = trueGraphX
// Enforcing max & min legend offsets
if (trueGraphX < legendWidth / 2) {
legendLeft = legendWidth / 2
} else if (trueGraphX > legendMaxLeft) {
legendLeft = legendMaxLeft
}
// Disallow screen overflow of legend
const isLegendBottomClipped = graphBottom + legendHeight > screenHeight
const isLegendTopClipped =
legendHeight > graphRect.top - chronografChromeSize
const willLegendFitLeft = e.pageX - chronografChromeSize > legendWidth
let legendTop = graphHeight + 8
this.setState({legendArrowPosition: 'top'})
// If legend is only clipped on the bottom, position above graph
if (isLegendBottomClipped && !isLegendTopClipped) {
this.setState({legendArrowPosition: 'bottom'})
legendTop = -legendHeight
}
// If legend is clipped on top and bottom, posiition on either side of crosshair
if (isLegendBottomClipped && isLegendTopClipped) {
legendTop = 0
if (willLegendFitLeft) {
this.setState({legendArrowPosition: 'right'})
legendLeft = trueGraphX - legendWidth / 2
legendLeft -= 8
} else {
this.setState({legendArrowPosition: 'left'})
legendLeft = trueGraphX + legendWidth / 2
legendLeft += 32
}
}
this.legendRef.style.left = `${legendLeft}px`
this.legendRef.style.top = `${legendTop}px`
this.setState({isHidden: false})
highlightCallback = ({pageX}) => {
this.setState({isHidden: false, pageX})
}
legendFormatter = legend => {
@ -424,12 +368,12 @@ export default class Dygraph extends Component {
render() {
const {
legend,
pageX,
sortType,
isHidden,
isSnipped,
filterText,
isAscending,
legendArrowPosition,
isFilterVisible,
} = this.state
@ -437,6 +381,9 @@ export default class Dygraph extends Component {
<div className="dygraph-child" onMouseLeave={this.deselectCrosshair}>
<DygraphLegend
{...legend}
graph={this.graphRef}
legend={this.legendRef}
pageX={pageX}
sortType={sortType}
onHide={this.handleHideLegend}
isHidden={isHidden}
@ -449,7 +396,6 @@ export default class Dygraph extends Component {
legendRef={this.handleLegendRef}
onToggleFilter={this.handleToggleFilter}
onInputChange={this.handleLegendInputChange}
arrowPosition={legendArrowPosition}
/>
<div
ref={r => {

View File

@ -2,6 +2,8 @@ import React, {PropTypes} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import {makeLegendStyles} from 'shared/graphs/helpers'
const removeMeasurement = (label = '') => {
const [measurement] = label.match(/^(.*)[.]/g) || ['']
return label.replace(measurement, '')
@ -9,6 +11,9 @@ const removeMeasurement = (label = '') => {
const DygraphLegend = ({
xHTML,
pageX,
graph,
legend,
series,
onSort,
onSnip,
@ -20,7 +25,6 @@ const DygraphLegend = ({
filterText,
isAscending,
onInputChange,
arrowPosition,
isFilterVisible,
onToggleFilter,
}) => {
@ -28,9 +32,11 @@ const DygraphLegend = ({
series,
({y, label}) => (sortType === 'numeric' ? y : label)
)
const ordered = isAscending ? sorted : sorted.reverse()
const filtered = ordered.filter(s => s.label.match(filterText))
const hidden = isHidden ? 'hidden' : ''
const style = makeLegendStyles(graph, legend, pageX)
const renderSortAlpha = (
<div
@ -65,9 +71,10 @@ const DygraphLegend = ({
return (
<div
className={`dygraph-legend dygraph-legend--${arrowPosition} ${hidden}`}
className={`dygraph-legend ${hidden}`}
ref={legendRef}
onMouseLeave={onHide}
style={style}
>
<div className="dygraph-legend--header">
<div className="dygraph-legend--timestamp">
@ -141,7 +148,9 @@ DygraphLegend.propTypes = {
yHTML: string,
})
),
dygraph: shape(),
pageX: number,
legend: shape({}),
graph: shape({}),
onSnip: func.isRequired,
onHide: func.isRequired,
onSort: func.isRequired,
@ -154,7 +163,6 @@ DygraphLegend.propTypes = {
legendRef: func.isRequired,
isSnipped: bool.isRequired,
isFilterVisible: bool.isRequired,
arrowPosition: string.isRequired,
}
export default DygraphLegend

View File

@ -114,6 +114,65 @@ export const barPlotter = e => {
}
}
export const makeLegendStyles = (graph, legend, pageX) => {
if (!graph || !legend || pageX === null) {
return {}
}
// Move the Legend on hover
const chronografChromeSize = 60 // Width & Height of navigation page elements
const graphRect = graph.getBoundingClientRect()
const legendRect = legend.getBoundingClientRect()
const graphWidth = graphRect.width + 32 // Factoring in padding from parent
const graphHeight = graphRect.height
const graphBottom = graphRect.bottom
const legendWidth = legendRect.width
const legendHeight = legendRect.height
const screenHeight = window.innerHeight
const legendMaxLeft = graphWidth - legendWidth / 2
const trueGraphX = pageX - graphRect.left
let legendLeft = trueGraphX
// Enforcing max & min legend offsets
if (trueGraphX < legendWidth / 2) {
legendLeft = legendWidth / 2
} else if (trueGraphX > legendMaxLeft) {
legendLeft = legendMaxLeft
}
// Disallow screen overflow of legend
const isLegendBottomClipped = graphBottom + legendHeight > screenHeight
const isLegendTopClipped = legendHeight > graphRect.top - chronografChromeSize
const willLegendFitLeft = pageX - chronografChromeSize > legendWidth
let legendTop = graphHeight + 8
// If legend is only clipped on the bottom, position above graph
if (isLegendBottomClipped && !isLegendTopClipped) {
legendTop = -legendHeight
}
// If legend is clipped on top and bottom, posiition on either side of crosshair
if (isLegendBottomClipped && isLegendTopClipped) {
legendTop = 0
if (willLegendFitLeft) {
legendLeft = trueGraphX - legendWidth / 2
legendLeft -= 8
} else {
legendLeft = trueGraphX + legendWidth / 2
legendLeft += 32
}
}
return {
left: `${legendLeft}px`,
top: `${legendTop}px`,
}
}
export const OPTIONS = {
rightGap: 0,
axisLineWidth: 2,