Merge branch 'master' into http-tcp

# Conflicts:
#	CHANGELOG.md
pull/864/head
Hunter Trujillo 2017-02-10 16:03:30 -07:00
commit 12e7688a49
12 changed files with 316 additions and 25 deletions

View File

@ -5,9 +5,10 @@
### Upcoming Features ### Upcoming Features
1. [#838](https://github.com/influxdata/chronograf/issues/838): Add detail node to kapacitor alerts 1. [#838](https://github.com/influxdata/chronograf/issues/838): Add detail node to kapacitor alerts
2. [#853](https://github.com/influxdata/chronograf/issues/853): Updated builds to use yarn over npm install 2. [#847](https://github.com/influxdata/chronograf/issues/847): Enable and disable kapacitor alerts from alert manager
3. [#860](https://github.com/influxdata/chronograf/issues/860): Add gzip encoding and caching of static assets to server 3. [#853](https://github.com/influxdata/chronograf/issues/853): Updated builds to use yarn over npm install
4. [#864](https://github.com/influxdata/chronograf/issues/864): Add support to kapacitor rule alert config for: 4. [#860](https://github.com/influxdata/chronograf/issues/860): Add gzip encoding and caching of static assets to server
5. [#864](https://github.com/influxdata/chronograf/issues/864): Add support to kapacitor rule alert config for:
- HTTP - HTTP
- TCP - TCP
- Exec - Exec

View File

@ -119,6 +119,44 @@ func (c *Client) Enable(ctx context.Context, href string) (*Task, error) {
return c.updateStatus(ctx, href, client.Enabled) return c.updateStatus(ctx, href, client.Enabled)
} }
// AllStatus returns the status of all tasks in kapacitor
func (c *Client) AllStatus(ctx context.Context) (map[string]string, error) {
kapa, err := c.kapaClient(ctx)
if err != nil {
return nil, err
}
// Only get the status, id and link section back
opts := &client.ListTasksOptions{
Fields: []string{"status"},
}
tasks, err := kapa.ListTasks(opts)
if err != nil {
return nil, err
}
taskStatuses := map[string]string{}
for _, task := range tasks {
taskStatuses[task.ID] = task.Status.String()
}
return taskStatuses, nil
}
// Status returns the status of a task in kapacitor
func (c *Client) Status(ctx context.Context, href string) (string, error) {
kapa, err := c.kapaClient(ctx)
if err != nil {
return "", err
}
task, err := kapa.Task(client.Link{Href: href}, nil)
if err != nil {
return "", err
}
return task.Status.String(), nil
}
// Update changes the tickscript of a given id. // Update changes the tickscript of a given id.
func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertRule) (*Task, error) { func (c *Client) Update(ctx context.Context, href string, rule chronograf.AlertRule) (*Task, error) {
kapa, err := c.kapaClient(ctx) kapa, err := c.kapaClient(ctx)

View File

@ -349,6 +349,7 @@ func (h *Service) KapacitorRulesPost(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)), Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)),
}, },
TICKScript: string(task.TICKScript), TICKScript: string(task.TICKScript),
Status: "enabled",
} }
w.Header().Add("Location", res.Links.Self) w.Header().Add("Location", res.Links.Self)
@ -364,6 +365,7 @@ type alertLinks struct {
type alertResponse struct { type alertResponse struct {
chronograf.AlertRule chronograf.AlertRule
TICKScript string `json:"tickscript"` TICKScript string `json:"tickscript"`
Status string `json:"status"`
Links alertLinks `json:"links"` Links alertLinks `json:"links"`
} }
@ -438,6 +440,92 @@ func (h *Service) KapacitorRulesPut(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)), Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)),
}, },
TICKScript: string(task.TICKScript), TICKScript: string(task.TICKScript),
Status: "enabled",
}
encodeJSON(w, http.StatusOK, res, h.Logger)
}
type KapacitorStatus struct {
Status string `json:"status"`
}
func (k *KapacitorStatus) Valid() error {
if k.Status == "enabled" || k.Status == "disabled" {
return nil
}
return fmt.Errorf("Invalid Kapacitor status: %s", k.Status)
}
// KapacitorRulesStatus proxies PATCH to kapacitor to enable/disable tasks
func (h *Service) KapacitorRulesStatus(w http.ResponseWriter, r *http.Request) {
id, err := paramID("kid", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
srcID, err := paramID("id", r)
if err != nil {
Error(w, http.StatusUnprocessableEntity, err.Error(), h.Logger)
return
}
ctx := r.Context()
srv, err := h.ServersStore.Get(ctx, id)
if err != nil || srv.SrcID != srcID {
notFound(w, id, h.Logger)
return
}
tid := httprouter.GetParamFromContext(ctx, "tid")
c := kapa.Client{
URL: srv.URL,
Username: srv.Username,
Password: srv.Password,
Ticker: &kapa.Alert{},
}
var req KapacitorStatus
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, h.Logger)
return
}
if err := req.Valid(); err != nil {
invalidData(w, err, h.Logger)
return
}
// Check if the rule exists and is scoped correctly
alert, err := h.AlertRulesStore.Get(ctx, srcID, id, tid)
if err != nil {
if err == chronograf.ErrAlertNotFound {
notFound(w, id, h.Logger)
return
}
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
var task *kapa.Task
if req.Status == "enabled" {
task, err = c.Enable(ctx, c.Href(tid))
} else {
task, err = c.Disable(ctx, c.Href(tid))
}
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
res := alertResponse{
AlertRule: alert,
Links: alertLinks{
Self: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/rules/%s", srv.SrcID, srv.ID, task.ID),
Kapacitor: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.Href)),
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(task.HrefOutput)),
},
TICKScript: string(task.TICKScript),
Status: req.Status,
} }
encodeJSON(w, http.StatusOK, res, h.Logger) encodeJSON(w, http.StatusOK, res, h.Logger)
} }
@ -470,7 +558,18 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
} }
ticker := &kapa.Alert{} ticker := &kapa.Alert{}
c := kapa.Client{} c := kapa.Client{
URL: srv.URL,
Username: srv.Username,
Password: srv.Password,
Ticker: ticker,
}
statuses, err := c.AllStatus(ctx)
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
res := allAlertsResponse{ res := allAlertsResponse{
Rules: []alertResponse{}, Rules: []alertResponse{},
} }
@ -481,6 +580,11 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
return return
} }
status, ok := statuses[rule.ID]
// The defined rule is not actually in kapacitor
if !ok {
continue
}
ar := alertResponse{ ar := alertResponse{
AlertRule: rule, AlertRule: rule,
Links: alertLinks{ Links: alertLinks{
@ -489,6 +593,7 @@ func (h *Service) KapacitorRulesGet(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(c.HrefOutput(rule.ID))), Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(c.HrefOutput(rule.ID))),
}, },
TICKScript: string(tickscript), TICKScript: string(tickscript),
Status: status,
} }
res.Rules = append(res.Rules, ar) res.Rules = append(res.Rules, ar)
} }
@ -532,13 +637,24 @@ func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
} }
ticker := &kapa.Alert{} ticker := &kapa.Alert{}
c := kapa.Client{} c := kapa.Client{
URL: srv.URL,
Username: srv.Username,
Password: srv.Password,
Ticker: ticker,
}
tickscript, err := ticker.Generate(rule) tickscript, err := ticker.Generate(rule)
if err != nil { if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger) Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return return
} }
status, err := c.Status(ctx, c.Href(rule.ID))
if err != nil {
Error(w, http.StatusInternalServerError, err.Error(), h.Logger)
return
}
res := alertResponse{ res := alertResponse{
AlertRule: rule, AlertRule: rule,
Links: alertLinks{ Links: alertLinks{
@ -547,6 +663,7 @@ func (h *Service) KapacitorRulesID(w http.ResponseWriter, r *http.Request) {
Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(c.HrefOutput(rule.ID))), Output: fmt.Sprintf("/chronograf/v1/sources/%d/kapacitors/%d/proxy?path=%s", srv.SrcID, srv.ID, url.QueryEscape(c.HrefOutput(rule.ID))),
}, },
TICKScript: string(tickscript), TICKScript: string(tickscript),
Status: status,
} }
encodeJSON(w, http.StatusOK, res, h.Logger) encodeJSON(w, http.StatusOK, res, h.Logger)
} }

View File

@ -42,10 +42,13 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
// Prefix any URLs found in the React assets with any configured basepath // Prefix any URLs found in the React assets with any configured basepath
prefixedAssets := NewDefaultURLPrefixer(basepath, assets, opts.Logger) 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 // The react application handles all the routing if the server does not
// know about the route. This means that we never have unknown // know about the route. This means that we never have unknown
// routes on the server. // routes on the server.
router.NotFound = prefixedAssets router.NotFound = compressed
/* Documentation */ /* Documentation */
router.GET("/swagger.json", Spec()) router.GET("/swagger.json", Spec())
@ -80,6 +83,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesID) 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.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) router.DELETE("/chronograf/v1/sources/:id/kapacitors/:kid/rules/:tid", service.KapacitorRulesDelete)
// Kapacitor Proxy // Kapacitor Proxy
@ -121,8 +125,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
return Logger(opts.Logger, auth) return Logger(opts.Logger, auth)
} }
compressed := gziphandler.GzipHandler(router) logged := Logger(opts.Logger, router)
logged := Logger(opts.Logger, compressed)
return logged return logged
} }

View File

@ -1851,6 +1851,14 @@
"type": "string", "type": "string",
"description": "TICKscript representing this rule" "description": "TICKscript representing this rule"
}, },
"status": {
"type": "string",
"description": "Represents if this rule is enabled or disabled in kapacitor",
"enum": [
"enabled",
"disabled"
]
},
"links": { "links": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -11,6 +11,7 @@ import {
updateAlertNodes, updateAlertNodes,
updateRuleName, updateRuleName,
deleteRuleSuccess, deleteRuleSuccess,
updateRuleStatusSuccess,
} from 'src/kapacitor/actions/view'; } from 'src/kapacitor/actions/view';
describe('Kapacitor.Reducers.rules', () => { describe('Kapacitor.Reducers.rules', () => {
@ -170,4 +171,20 @@ describe('Kapacitor.Reducers.rules', () => {
const newState = reducer(initialState, updateDetails(ruleID, details)); const newState = reducer(initialState, updateDetails(ruleID, details));
expect(newState[ruleID].details).to.equal(details); expect(newState[ruleID].details).to.equal(details);
}); });
it('can update status', () => {
const ruleID = 1;
const status = 'enabled';
const initialState = {
[ruleID]: {
id: ruleID,
queryID: 988,
status: 'disabled',
}
};
const newState = reducer(initialState, updateRuleStatusSuccess(ruleID, status));
expect(newState[ruleID].status).to.equal(status);
});
}); });

View File

@ -1,7 +1,12 @@
import uuid from 'node-uuid'; import uuid from 'node-uuid';
import {getRules, getRule, deleteRule as deleteRuleAPI} from 'src/kapacitor/apis';
import {getKapacitor} from 'src/shared/apis'; import {getKapacitor} from 'src/shared/apis';
import {publishNotification} from 'src/shared/actions/notifications'; import {publishNotification} from 'src/shared/actions/notifications';
import {
getRules,
getRule,
deleteRule as deleteRuleAPI,
updateRuleStatus as updateRuleStatusAPI,
} from 'src/kapacitor/apis';
export function fetchRule(source, ruleID) { export function fetchRule(source, ruleID) {
return (dispatch) => { return (dispatch) => {
@ -137,6 +142,16 @@ export function deleteRuleSuccess(ruleID) {
}; };
} }
export function updateRuleStatusSuccess(ruleID, status) {
return {
type: 'UPDATE_RULE_STATUS_SUCCESS',
payload: {
ruleID,
status,
},
};
}
export function deleteRule(rule) { export function deleteRule(rule) {
return (dispatch) => { return (dispatch) => {
deleteRuleAPI(rule).then(() => { deleteRuleAPI(rule).then(() => {
@ -147,3 +162,14 @@ export function deleteRule(rule) {
}); });
}; };
} }
export function updateRuleStatus(rule, {status}) {
return (dispatch) => {
updateRuleStatusAPI(rule, status).then(() => {
dispatch(publishNotification('success', `${rule.name} ${status} successfully`));
}).catch(() => {
dispatch(updateRuleStatusSuccess(rule.id, status));
dispatch(publishNotification('error', `${rule.name} could not be ${status}`));
});
};
}

View File

@ -47,3 +47,11 @@ export function deleteRule(rule) {
url: rule.links.self, url: rule.links.self,
}); });
} }
export function updateRuleStatus(rule, status) {
return AJAX({
method: 'PATCH',
url: rule.links.self,
data: {status},
});
}

View File

@ -6,27 +6,35 @@ import {getKapacitor} from 'src/shared/apis';
import * as kapacitorActionCreators from '../actions/view'; import * as kapacitorActionCreators from '../actions/view';
import NoKapacitorError from '../../shared/components/NoKapacitorError'; import NoKapacitorError from '../../shared/components/NoKapacitorError';
const {
arrayOf,
func,
shape,
string,
} = PropTypes;
export const KapacitorRulesPage = React.createClass({ export const KapacitorRulesPage = React.createClass({
propTypes: { propTypes: {
source: PropTypes.shape({ source: shape({
id: PropTypes.string.isRequired, id: string.isRequired,
links: PropTypes.shape({ links: shape({
proxy: PropTypes.string.isRequired, proxy: string.isRequired,
self: PropTypes.string.isRequired, self: string.isRequired,
kapacitors: PropTypes.string.isRequired, kapacitors: string.isRequired,
}), }),
}), }),
rules: PropTypes.arrayOf(PropTypes.shape({ rules: arrayOf(shape({
name: PropTypes.string.isRequired, name: string.isRequired,
trigger: PropTypes.string.isRequired, trigger: string.isRequired,
message: PropTypes.string.isRequired, message: string.isRequired,
alerts: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, alerts: arrayOf(string.isRequired).isRequired,
})).isRequired, })).isRequired,
actions: PropTypes.shape({ actions: shape({
fetchRules: PropTypes.func.isRequired, fetchRules: func.isRequired,
deleteRule: PropTypes.func.isRequired, deleteRule: func.isRequired,
updateRuleStatus: func.isRequired,
}).isRequired, }).isRequired,
addFlashMessage: PropTypes.func, addFlashMessage: func,
}, },
getInitialState() { getInitialState() {
@ -50,6 +58,14 @@ export const KapacitorRulesPage = React.createClass({
actions.deleteRule(rule); actions.deleteRule(rule);
}, },
handleRuleStatus(e, rule) {
const {actions} = this.props;
const status = e.target.checked ? 'enabled' : 'disabled';
actions.updateRuleStatusSuccess(rule.id, status);
actions.updateRuleStatus(rule, {status});
},
renderSubComponent() { renderSubComponent() {
const {source} = this.props; const {source} = this.props;
const {hasKapacitor, loading} = this.state; const {hasKapacitor, loading} = this.state;
@ -72,6 +88,7 @@ export const KapacitorRulesPage = React.createClass({
<th>Trigger</th> <th>Trigger</th>
<th>Message</th> <th>Message</th>
<th>Alerts</th> <th>Alerts</th>
<th className="text-center">Enabled</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -129,6 +146,12 @@ export const KapacitorRulesPage = React.createClass({
<td className="monotype">{rule.trigger}</td> <td className="monotype">{rule.trigger}</td>
<td className="monotype">{rule.message}</td> <td className="monotype">{rule.message}</td>
<td className="monotype">{rule.alerts.join(', ')}</td> <td className="monotype">{rule.alerts.join(', ')}</td>
<td className="monotype text-center">
<div className="dark-checkbox">
<input id="kapacitor-enabled" className="form-control-static" type="checkbox" ref={(r) => this.enabled = r} checked={rule.status === "enabled"} onClick={(e) => this.handleRuleStatus(e, rule)} />
<label htmlFor="kapacitor-enabled"></label>
</div>
</td>
<td className="text-right"><button className="btn btn-danger btn-xs" onClick={() => this.handleDeleteRule(rule)}>Delete</button></td> <td className="text-right"><button className="btn btn-danger btn-xs" onClick={() => this.handleDeleteRule(rule)}>Delete</button></td>
</tr> </tr>
); );

View File

@ -143,6 +143,14 @@ export default function rules(state = {}, action) {
[ruleID]: {...state[ruleID], details}, [ruleID]: {...state[ruleID], details},
}}; }};
} }
case 'UPDATE_RULE_STATUS_SUCCESS': {
const {ruleID, status} = action.payload;
return {...state, ...{
[ruleID]: {...state[ruleID], status},
}};
}
} }
return state; return state;
} }

View File

@ -78,7 +78,6 @@
padding: 9.5px 0 0 0; padding: 9.5px 0 0 0;
background-color: $query-editor-tab-active; background-color: $query-editor-tab-active;
border-radius: 0 0 $radius-small $radius-small; border-radius: 0 0 $radius-small $radius-small;
min-height: $query-editor-height;
&-item { &-item {
color: $g11-sidewalk; color: $g11-sidewalk;

View File

@ -611,4 +611,47 @@ $form-static-checkbox-size: 16px;
} }
} }
} }
}
.dark-checkbox {
input {
position: absolute;
left: -9999px;
visibility: hidden;
}
label {
display: inline-block;
width: $form-static-checkbox-size;
height: $form-static-checkbox-size;
background-color: $g1-raven;
border-radius: $radius-small;
position: relative;
vertical-align: middle;
margin: 0;
transition: background-color 0.25s ease;
}
label:hover {
cursor: pointer;
background-color: $g2-kevlar;
}
label:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 6px;
height: 6px;
background-color: $c-pool;
border-radius: 50%;
transform: translate(-50%,-50%) scale(2,2);
opacity: 0;
z-index: 3;
transition:
opacity 0.25s ease,
transform 0.25s ease;
}
input:checked + label:after {
opacity: 1;
transform: translate(-50%,-50%) scale(1,1);
}
} }