diff --git a/CHANGELOG.md b/CHANGELOG.md index 400c876cf..1682130cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,26 @@ ## v1.3.4.0 [unreleased] - ### Bug Fixes ### Features -### UI Improvements +1. [#1645](https://github.com/influxdata/chronograf/pull/1645): Add Auth0 as a supported OAuth2 provider -## v1.3.3.1 [2017-06-20] +### UI Improvements +1. [#1644](https://github.com/influxdata/chronograf/pull/1644): Redesign Alerts History table to have sticky headers +1. [#1581](https://github.com/influxdata/chronograf/pull/1581): Refresh template variable values on dashboard page load + +## v1.3.3.3 [2017-06-21] ### Bug Fixes +1. [1651](https://github.com/influxdata/chronograf/pull/1651): Add back in x and y axes and revert some style changes on Line + Single Stat graphs + +## v1.3.3.2 [2017-06-21] +### Bug Fixes +1. [1650](https://github.com/influxdata/chronograf/pull/1650): Fix broken cpu reporting on hosts page and normalize InfluxQL + +## v1.3.3.1 [2017-06-21] +### Bug Fixes +1. [#1641](https://github.com/influxdata/chronograf/pull/1641): Fix enable / disable being out of sync on Kapacitor Rules Page + +### Features +### UI Improvements 1. [#1642](https://github.com/influxdata/chronograf/pull/1642): Do not prefix basepath to external link for news feed ## v1.3.3.0 [2017-06-19] diff --git a/kapacitor/client.go b/kapacitor/client.go index 378f0efab..3f516d8ee 100644 --- a/kapacitor/client.go +++ b/kapacitor/client.go @@ -171,16 +171,25 @@ func (c *Client) AllStatus(ctx context.Context) (map[string]string, error) { // Status returns the status of a task in kapacitor func (c *Client) Status(ctx context.Context, href string) (string, error) { - kapa, err := c.kapaClient(c.URL, c.Username, c.Password) - if err != nil { - return "", err - } - task, err := kapa.Task(client.Link{Href: href}, nil) + s, err := c.status(ctx, href) if err != nil { return "", err } - return task.Status.String(), nil + return s.String(), nil +} + +func (c *Client) status(ctx context.Context, href string) (client.TaskStatus, error) { + kapa, err := c.kapaClient(c.URL, c.Username, c.Password) + if err != nil { + return 0, err + } + task, err := kapa.Task(client.Link{Href: href}, nil) + if err != nil { + return 0, err + } + + return task.Status, nil } // All returns all tasks in kapacitor @@ -259,6 +268,11 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR return nil, err } + prevStatus, err := c.status(ctx, href) + if err != nil { + return nil, err + } + // We need to disable the kapacitor task followed by enabling it during update. opts := client.UpdateTaskOptions{ TICKscript: string(script), @@ -277,9 +291,11 @@ func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertR return nil, err } - // Now enable the task. - if _, err := c.Enable(ctx, href); err != nil { - return nil, err + // Now enable the task if previously enabled + if prevStatus == client.Enabled { + if _, err := c.Enable(ctx, href); err != nil { + return nil, err + } } return &Task{ diff --git a/kapacitor/client_test.go b/kapacitor/client_test.go index 7a1c0bcec..d3850a4e7 100644 --- a/kapacitor/client_test.go +++ b/kapacitor/client_test.go @@ -11,9 +11,14 @@ import ( ) type MockKapa struct { - ResTask client.Task - ResTasks []client.Task - Error error + ResTask client.Task + ResTasks []client.Task + TaskError error + UpdateError error + CreateError error + ListError error + DeleteError error + LastStatus client.TaskStatus *client.CreateTaskOptions client.Link @@ -24,31 +29,34 @@ type MockKapa struct { func (m *MockKapa) CreateTask(opt client.CreateTaskOptions) (client.Task, error) { m.CreateTaskOptions = &opt - return m.ResTask, m.Error + return m.ResTask, m.CreateError } func (m *MockKapa) Task(link client.Link, opt *client.TaskOptions) (client.Task, error) { m.Link = link m.TaskOptions = opt - return m.ResTask, m.Error + return m.ResTask, m.TaskError } func (m *MockKapa) ListTasks(opt *client.ListTasksOptions) ([]client.Task, error) { m.ListTasksOptions = opt - return m.ResTasks, m.Error + return m.ResTasks, m.ListError } func (m *MockKapa) UpdateTask(link client.Link, opt client.UpdateTaskOptions) (client.Task, error) { m.Link = link + m.LastStatus = opt.Status + if m.UpdateTaskOptions == nil { m.UpdateTaskOptions = &opt } - return m.ResTask, m.Error + + return m.ResTask, m.UpdateError } func (m *MockKapa) DeleteTask(link client.Link) error { m.Link = link - return m.Error + return m.DeleteError } type MockID struct { @@ -150,7 +158,7 @@ func TestClient_AllStatus(t *testing.T) { for _, tt := range tests { kapa.ResTask = tt.resTask kapa.ResTasks = tt.resTasks - kapa.Error = tt.resError + kapa.ListError = tt.resError t.Run(tt.name, func(t *testing.T) { c := &Client{ @@ -426,7 +434,7 @@ trigger for _, tt := range tests { kapa.ResTask = tt.resTask kapa.ResTasks = tt.resTasks - kapa.Error = tt.resError + kapa.ListError = tt.resError t.Run(tt.name, func(t *testing.T) { c := &Client{ URL: tt.fields.URL, @@ -710,7 +718,7 @@ trigger for _, tt := range tests { kapa.ResTask = tt.resTask kapa.ResTasks = tt.resTasks - kapa.Error = tt.resError + kapa.TaskError = tt.resError t.Run(tt.name, func(t *testing.T) { c := &Client{ URL: tt.fields.URL, @@ -854,7 +862,7 @@ func TestClient_updateStatus(t *testing.T) { } for _, tt := range tests { kapa.ResTask = tt.resTask - kapa.Error = tt.resError + kapa.UpdateError = tt.resError kapa.UpdateTaskOptions = nil t.Run(tt.name, func(t *testing.T) { c := &Client{ @@ -904,6 +912,7 @@ func TestClient_Update(t *testing.T) { resError error wantErr bool updateTaskOptions *client.UpdateTaskOptions + wantStatus client.TaskStatus }{ { name: "update alert rule error", @@ -936,7 +945,8 @@ func TestClient_Update(t *testing.T) { }, }, }, - wantErr: true, + wantErr: true, + wantStatus: client.Disabled, }, { name: "update alert rule", @@ -984,11 +994,60 @@ func TestClient_Update(t *testing.T) { Name: "howdy", }, }, + wantStatus: client.Enabled, + }, + { + name: "stays disabled when already disabled", + fields: fields{ + kapaClient: func(url, username, password string) (KapaClient, error) { + return kapa, nil + }, + Ticker: &Alert{}, + }, + args: args{ + ctx: context.Background(), + href: "/kapacitor/v1/tasks/howdy", + rule: chronograf.AlertRule{ + ID: "howdy", + Query: &chronograf.QueryConfig{ + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + resTask: client.Task{ + ID: "howdy", + Status: client.Disabled, + Link: client.Link{ + Href: "/kapacitor/v1/tasks/howdy", + }, + }, + updateTaskOptions: &client.UpdateTaskOptions{ + TICKscript: "", + Type: client.StreamTask, + Status: client.Disabled, + DBRPs: []client.DBRP{ + { + Database: "db", + RetentionPolicy: "rp", + }, + }, + }, + want: &Task{ + ID: "howdy", + Href: "/kapacitor/v1/tasks/howdy", + HrefOutput: "/kapacitor/v1/tasks/howdy/output", + Rule: chronograf.AlertRule{ + ID: "howdy", + Name: "howdy", + }, + }, + wantStatus: client.Disabled, }, } for _, tt := range tests { kapa.ResTask = tt.resTask - kapa.Error = tt.resError + kapa.UpdateError = tt.resError t.Run(tt.name, func(t *testing.T) { c := &Client{ URL: tt.fields.URL, @@ -1009,6 +1068,9 @@ func TestClient_Update(t *testing.T) { if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) { t.Errorf("Client.Update() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions) } + if tt.wantStatus != kapa.LastStatus { + t.Errorf("Client.Update() = %v, want %v", kapa.LastStatus, tt.wantStatus) + } }) } } @@ -1126,7 +1188,7 @@ func TestClient_Create(t *testing.T) { } for _, tt := range tests { kapa.ResTask = tt.resTask - kapa.Error = tt.resError + kapa.CreateError = tt.resError t.Run(tt.name, func(t *testing.T) { c := &Client{ URL: tt.fields.URL, diff --git a/oauth2/auth0.go b/oauth2/auth0.go new file mode 100644 index 000000000..a1662d380 --- /dev/null +++ b/oauth2/auth0.go @@ -0,0 +1,47 @@ +package oauth2 + +import ( + "net/url" + + "github.com/influxdata/chronograf" +) + +type Auth0 struct { + Generic +} + +func NewAuth0(auth0Domain, clientID, clientSecret, redirectURL string, logger chronograf.Logger) (Auth0, error) { + domain, err := url.Parse(auth0Domain) + if err != nil { + return Auth0{}, err + } + + domain.Scheme = "https" + + domain.Path = "/authorize" + authURL := domain.String() + + domain.Path = "/oauth/token" + tokenURL := domain.String() + + domain.Path = "/userinfo" + apiURL := domain.String() + + return Auth0{ + Generic: Generic{ + PageName: "auth0", + + ClientID: clientID, + ClientSecret: clientSecret, + + RequiredScopes: []string{"openid"}, + + RedirectURL: redirectURL, + AuthURL: authURL, + TokenURL: tokenURL, + APIURL: apiURL, + + Logger: logger, + }, + }, nil +} diff --git a/server/server.go b/server/server.go index 8ce9bb3f3..2e60219c9 100644 --- a/server/server.go +++ b/server/server.go @@ -80,6 +80,10 @@ type Server struct { StatusFeedURL string `long:"status-feed-url" description:"URL of a JSON Feed to display as a News Feed on the client Status page." default:"https://www.influxdata.com/feed/json" env:"STATUS_FEED_URL"` + Auth0Domain string `long:"auth0-domain" description:"Subdomain of auth0.com used for Auth0 OAuth2 authentication" env:"AUTH0_DOMAIN"` + Auth0ClientID string `long:"auth0-client-id" description:"Auth0 Client ID for OAuth2 support" env:"AUTH0_CLIENT_ID"` + Auth0ClientSecret string `long:"auth0-client-secret" description:"Auth0 Client Secret for OAuth2 support" env:"AUTH0_CLIENT_SECRET"` + 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:"error" default:"info" description:"Set the logging level" env:"LOG_LEVEL"` Basepath string `short:"p" long:"basepath" description:"A URL path prefix under which all chronograf routes will be mounted" env:"BASE_PATH"` @@ -113,6 +117,10 @@ func (s *Server) UseHeroku() bool { return s.TokenSecret != "" && s.HerokuClientID != "" && s.HerokuSecret != "" } +func (s *Server) UseAuth0() bool { + return s.Auth0ClientID != "" && s.Auth0ClientSecret != "" +} + // UseGenericOAuth2 validates the CLI parameters to enable generic oauth support func (s *Server) UseGenericOAuth2() bool { return s.TokenSecret != "" && s.GenericClientID != "" && @@ -176,6 +184,27 @@ func (s *Server) genericOAuth(logger chronograf.Logger, auth oauth2.Authenticato return &gen, genMux, s.UseGenericOAuth2 } +func (s *Server) auth0OAuth(logger chronograf.Logger, auth oauth2.Authenticator) (oauth2.Provider, oauth2.Mux, func() bool) { + redirectPath := path.Join(s.Basepath, "oauth", "auth0", "callback") + redirectURL, err := url.Parse(s.PublicURL) + if err != nil { + logger.Error("Error parsing public URL: err:", err) + return &oauth2.Auth0{}, &oauth2.AuthMux{}, func() bool { return false } + } + redirectURL.Path = redirectPath + + auth0, err := oauth2.NewAuth0(s.Auth0Domain, s.Auth0ClientID, s.Auth0ClientSecret, redirectURL.String(), logger) + + jwt := oauth2.NewJWT(s.TokenSecret) + genMux := oauth2.NewAuthMux(&auth0, auth, jwt, s.Basepath, logger) + + if err != nil { + logger.Error("Error parsing Auth0 domain: err:", err) + return &auth0, genMux, func() bool { return false } + } + return &auth0, genMux, s.UseAuth0 +} + func (s *Server) genericRedirectURL() string { if s.PublicURL == "" { return "" @@ -202,7 +231,7 @@ type BuildInfo struct { } func (s *Server) useAuth() bool { - return s.UseGithub() || s.UseGoogle() || s.UseHeroku() || s.UseGenericOAuth2() + return s.UseGithub() || s.UseGoogle() || s.UseHeroku() || s.UseGenericOAuth2() || s.UseAuth0() } func (s *Server) useTLS() bool { @@ -273,6 +302,7 @@ func (s *Server) Serve(ctx context.Context) error { providerFuncs = append(providerFuncs, provide(s.googleOAuth(logger, auth))) providerFuncs = append(providerFuncs, provide(s.herokuOAuth(logger, auth))) providerFuncs = append(providerFuncs, provide(s.genericOAuth(logger, auth))) + providerFuncs = append(providerFuncs, provide(s.auth0OAuth(logger, auth))) s.handler = NewMux(MuxOpts{ Develop: s.Develop, diff --git a/ui/src/alerts/components/AlertsTable.js b/ui/src/alerts/components/AlertsTable.js index cc9e573df..22625449e 100644 --- a/ui/src/alerts/components/AlertsTable.js +++ b/ui/src/alerts/components/AlertsTable.js @@ -2,6 +2,10 @@ import React, {Component, PropTypes} from 'react' import _ from 'lodash' import {Link} from 'react-router' +import FancyScrollbar from 'shared/components/FancyScrollbar' + +import {ALERTS_TABLE} from 'src/alerts/constants/tableSizing' + class AlertsTable extends Component { constructor(props) { super(props) @@ -55,11 +59,11 @@ class AlertsTable extends Component { sortableClasses(key) { if (this.state.sortKey === key) { if (this.state.sortDirection === 'asc') { - return 'sortable-header sorting-ascending' + return 'alert-history-table--th sortable-header sorting-ascending' } - return 'sortable-header sorting-descending' + return 'alert-history-table--th sortable-header sorting-descending' } - return 'sortable-header' + return 'alert-history-table--th sortable-header' } sort(alerts, key, direction) { @@ -80,64 +84,93 @@ class AlertsTable extends Component { this.state.sortKey, this.state.sortDirection ) + const {colName, colLevel, colTime, colHost, colValue} = ALERTS_TABLE return this.props.alerts.length - ?
this.changeSort('name')} - className={this.sortableClasses('name')} - > - Name - | -this.changeSort('level')} - className={this.sortableClasses('level')} - > - Level - | -this.changeSort('time')} - className={this.sortableClasses('time')} - > - Time - | -this.changeSort('host')} - className={this.sortableClasses('host')} - > - Host - | -this.changeSort('value')} - className={this.sortableClasses('value')} - > - Value - | -
---|---|---|---|---|
{name} | -
+
+
+ {name}
+
+
{level}
- |
-
+
+
{new Date(Number(time)).toISOString()}
- |
-
+
+
{host}
- |
- {value} | -