Skip to content

Commit 026600e

Browse files
author
Khanh Nguyen
committed
Added support for rule templates
1 parent 7f75560 commit 026600e

File tree

11 files changed

+390
-4
lines changed

11 files changed

+390
-4
lines changed

README.md

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,25 @@ docker build . -t elastalert
1313
Then, run it, optionally with your custom ElastAlert configuration file. We've included a sensible default, with localhost as ES host.
1414

1515
_Bash_
16-
```Bash
17-
docker run -d -p 3030:3030 -v `pwd`/config/elastalert.yaml:/opt/elastalert/config.yaml -v `pwd`/config/elastalert-server.json:/opt/elastalert-server/config/config.json -v (pwd)/rules:/opt/elastalert/rules -v (pwd)/server-data:/opt/elastalert/server_dat --net="host" elastalert:latest
16+
```bash
17+
docker run -d -p 3030:3030 \
18+
-v `pwd`/config/elastalert.yaml:/opt/elastalert/config.yaml \
19+
-v `pwd`/config/elastalert-server.json:/opt/elastalert-server/config/config.json \
20+
-v `pwd`/rules:/opt/elastalert/rules \
21+
-v `pwd`/rule_templates:/opt/elastalert/rule_templates \
22+
-v `pwd`/server-data:/opt/elastalert/server_dat \
23+
--net="host" elastalert:latest
1824
```
1925

2026
_Fish_
2127
```fish
22-
docker run -ti -p 3030:3030 -v (pwd)/config/elastalert.yaml:/opt/elastalert/config.yaml -v (pwd)/config/elastalert-server.json:/opt/elastalert-server/config/config.json -v (pwd)/rules:/opt/elastalert/rules -v (pwd)/server-data:/opt/elastalert/server_data --net="host" elastalert:latest
28+
docker run -d -p 3030:3030 \
29+
-v (pwd)/config/elastalert.yaml:/opt/elastalert/config.yaml \
30+
-v (pwd)/config/elastalert-server.json:/opt/elastalert-server/config/config.json \
31+
-v (pwd)/rules:/opt/elastalert/rules \
32+
-v (pwd)/rule_templates:/opt/elastalert/rule_templates \
33+
-v (pwd)/server-data:/opt/elastalert/server_data \
34+
--net="host" elastalert:latest
2335
```
2436

2537
## Installation using npm and manual ElastAlert setup
@@ -67,6 +79,16 @@ You can use the following config options:
6779
// The path to the rules folder.
6880
"path": "/rules"
6981
},
82+
83+
// The path to the rules folder containing all the rules. If the folder is empty a dummy file will be created to allow ElastAlert to start.
84+
"templatesPath": {
85+
86+
// Whether to use a path relative to the `elastalertPath` folder.
87+
"relative": true,
88+
89+
// The path to the rules folder.
90+
"path": "/rule_templates"
91+
},
7092

7193
// The path to a folder that the server can use to store data and temporary files.
7294
"dataPath": {
@@ -121,6 +143,29 @@ This server exposes the following REST API's:
121143
- **DELETE `/rules/:id`**
122144

123145
Where `:id` is the id of the rule returned by **GET `/rules`**, which will delete the given rule.
146+
147+
- **GET `/templates`**
148+
149+
Returns a list of directories and templates that exist in the `templatesPath` (from the config) and are being run by the ElastAlert process.
150+
151+
- **GET `/templates/:id`**
152+
153+
Where `:id` is the id of the template returned by **GET `/templates`**, which will return the file contents of that template.
154+
155+
- **POST `/templates/:id`**
156+
157+
Where `:id` is the id of the template returned by **GET `/templates`**, which will allow you to edit the template. The body send should be:
158+
159+
```javascript
160+
{
161+
// Required - The full yaml template config.
162+
"yaml": "..."
163+
}
164+
```
165+
166+
- **DELETE `/templates/:id`**
167+
168+
Where `:id` is the id of the template returned by **GET `/templates`**, which will delete the given template.
124169

125170
- **POST `/test`**
126171

config/elastalert-server.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@
55
"rulesPath": {
66
"relative": true,
77
"path": "/rules"
8+
},
9+
"templatesPath": {
10+
"relative":true,
11+
"path":"/rule_templates"
812
}
9-
}
13+
}

src/common/config/schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ const schema = Joi.object().keys({
99
'relative': Joi.boolean().default(true),
1010
'path': Joi.string().default('/rules')
1111
}).default(),
12+
'templatesPath': Joi.object().keys({
13+
'relative': Joi.boolean().default(true),
14+
'path': Joi.string().default('/rule_templates')
15+
}).default(),
1216
'dataPath': Joi.object().keys({
1317
'relative': Joi.boolean().default(true),
1418
'path': Joi.string().default('/server_data')
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import RequestError from './request_error';
2+
3+
export class TemplateNotFoundError extends RequestError {
4+
constructor(templateID) {
5+
super('templateNotFound', `The requested template with id: '${templateID}' couldn't be found.`, 404);
6+
}
7+
}
8+
9+
export class TemplateNotReadableError extends RequestError {
10+
constructor(templateID) {
11+
super('templateNotReadable', `The requested template with id: '${templateID}' isn't readable by the file system.`, 403);
12+
}
13+
}
14+
15+
export class TemplateNotWritableError extends RequestError {
16+
constructor(templateID) {
17+
super('templateNotWritable', `The requested template with id: '${templateID}' isn't writable by the file system.`, 403);
18+
}
19+
}
20+
21+
export class TemplatesFolderNotFoundError extends RequestError {
22+
constructor(path) {
23+
super('templatesFolderNotFound', `The requested folder with path: '${path}' couldn't be found.`, 404);
24+
}
25+
}
26+
27+
export class TemplatesFolderNotReadableError extends RequestError {
28+
constructor(path) {
29+
super('templatesFolderNotReadable', `The requested folder with path: '${path}' isn't readable by the file system.`, 403);
30+
}
31+
}
32+
33+
export class TemplatesFolderNotWritableError extends RequestError {
34+
constructor(path) {
35+
super('templatesFolderNotWritable', `The requested folder with path: '${path}' isn't writable by the file system.`, 403);
36+
}
37+
}
38+
39+
export class TemplatesRootFolderNotCreatableError extends RequestError {
40+
constructor() {
41+
super('templatesRootFolderNotCreatable', 'The templates folder wasn\'t found and couldn\'t be created by the file system.', 403);
42+
}
43+
}

src/controllers/templates/index.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {join as joinPath, normalize as normalizePath, extname as pathExtension} from 'path';
2+
import mkdirp from 'mkdirp';
3+
import FileSystem from '../../common/file_system';
4+
import config from '../../common/config';
5+
import Logger from '../../common/logger';
6+
import {TemplateNotFoundError, TemplateNotReadableError, TemplateNotWritableError,
7+
TemplatesFolderNotFoundError, TemplatesRootFolderNotCreatableError} from '../../common/errors/template_request_errors';
8+
9+
let logger = new Logger('TemplatesController');
10+
11+
export default class TemplatesController {
12+
constructor() {
13+
this._fileSystemController = new FileSystem();
14+
this.templatesFolder = this._getTemplatesFolder();
15+
}
16+
17+
getTemplates(path) {
18+
const self = this;
19+
const fullPath = joinPath(self.templatesFolder, path);
20+
return new Promise(function (resolve, reject) {
21+
self._fileSystemController.readDirectory(fullPath)
22+
.then(function (directoryIndex) {
23+
24+
directoryIndex.templates = directoryIndex.files.filter(function (fileName) {
25+
return pathExtension(fileName).toLowerCase() === '.yaml';
26+
}).map(function (fileName) {
27+
return fileName.slice(0, -5);
28+
});
29+
30+
delete directoryIndex.files;
31+
resolve(directoryIndex);
32+
})
33+
.catch(function (error) {
34+
35+
// Check if the requested folder is the templates root folder
36+
if (normalizePath(self.templatesFolder) === fullPath) {
37+
38+
// Try to create the root folder
39+
mkdirp(fullPath, function (error) {
40+
if (error) {
41+
reject(new TemplatesRootFolderNotCreatableError());
42+
logger.warn(`The templates root folder (${fullPath}) couldn't be found nor could it be created by the file system.`);
43+
} else {
44+
resolve(self._fileSystemController.getEmptyDirectoryIndex());
45+
}
46+
});
47+
} else {
48+
logger.warn(`The requested folder (${fullPath}) couldn't be found / read by the server. Error:`, error);
49+
reject(new TemplatesFolderNotFoundError(path));
50+
}
51+
});
52+
});
53+
}
54+
55+
template(id) {
56+
const self = this;
57+
return new Promise(function (resolve, reject) {
58+
self._findTemplate(id)
59+
.then(function (access) {
60+
console.log('template resolved');
61+
resolve({
62+
get: function () {
63+
if (access.read) {
64+
return self._getTemplate(id);
65+
}
66+
return self._getErrorPromise(new TemplateNotReadableError(id));
67+
},
68+
edit: function (body) {
69+
if (access.write) {
70+
return self._editTemplate(id, body);
71+
}
72+
return self._getErrorPromise(new TemplateNotWritableError(id));
73+
},
74+
delete: function () {
75+
return self._deleteTemplate(id);
76+
}
77+
});
78+
})
79+
.catch(function () {
80+
console.log('catched');
81+
reject(new TemplateNotFoundError(id));
82+
});
83+
});
84+
}
85+
86+
createTemplate(id, content) {
87+
return this._editTemplate(id, content);
88+
}
89+
90+
_findTemplate(id) {
91+
let fileName = id + '.yaml';
92+
const self = this;
93+
return new Promise(function (resolve, reject) {
94+
self._fileSystemController.fileExists(joinPath(self.templatesFolder, fileName))
95+
.then(function (exists) {
96+
if (!exists) {
97+
reject();
98+
} else {
99+
//TODO: Get real permissions
100+
//resolve(permissions);
101+
resolve({
102+
read: true,
103+
write: true
104+
});
105+
}
106+
})
107+
.catch(function (error) {
108+
reject(error);
109+
});
110+
});
111+
}
112+
113+
_getTemplate(id) {
114+
const path = joinPath(this.templatesFolder, id + '.yaml');
115+
return this._fileSystemController.readFile(path);
116+
}
117+
118+
_editTemplate(id, body) {
119+
const path = joinPath(this.templatesFolder, id + '.yaml');
120+
return this._fileSystemController.writeFile(path, body);
121+
}
122+
123+
_deleteTemplate(id) {
124+
const path = joinPath(this.templatesFolder, id + '.yaml');
125+
return this._fileSystemController.deleteFile(path);
126+
}
127+
128+
_getErrorPromise(error) {
129+
return new Promise(function (resolve, reject) {
130+
reject(error);
131+
});
132+
}
133+
134+
_getTemplatesFolder() {
135+
const templateFolderSettings = config.get('templatesPath');
136+
137+
if (templateFolderSettings.relative) {
138+
return joinPath(config.get('elastalertPath'), templateFolderSettings.path);
139+
} else {
140+
return templateFolderSettings.path;
141+
}
142+
}
143+
}

src/elastalert_server.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import FileSystem from './common/file_system';
77
import setupRouter from './routes/route_setup';
88
import ProcessController from './controllers/process';
99
import RulesController from './controllers/rules';
10+
import TemplatesController from './controllers/templates';
1011
import TestController from './controllers/test';
1112

1213
let logger = new Logger('Server');
@@ -17,6 +18,7 @@ export default class ElastalertServer {
1718
this._runningTimeouts = [];
1819
this._processController = null;
1920
this._rulesController = null;
21+
this._templatesController = null;
2022

2123
// Set listener on process exit (SIGINT == ^C)
2224
process.on('SIGINT', () => {
@@ -42,6 +44,10 @@ export default class ElastalertServer {
4244
return this._rulesController;
4345
}
4446

47+
get templatesController() {
48+
return this._templatesController;
49+
}
50+
4551
get testController() {
4652
return this._testController;
4753
}
@@ -63,6 +69,7 @@ export default class ElastalertServer {
6369
self._processController.start();
6470

6571
self._rulesController = new RulesController();
72+
self._templatesController = new TemplatesController();
6673
self._testController = new TestController(self);
6774

6875
self._fileSystemController.createDirectoryIfNotExists(self.getDataFolder()).catch(function (error) {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import RouteLogger from '../../../routes/route_logger';
2+
import {sendRequestError} from '../../../common/errors/utils';
3+
4+
let logger = new RouteLogger('/templates/:id', 'DELETE');
5+
6+
export default function templateDeleteHandler(request, response) {
7+
/**
8+
* @type {ElastalertServer}
9+
*/
10+
let server = request.app.get('server');
11+
12+
server.templatesController.template(request.params.id)
13+
.then(function (template) {
14+
template.delete()
15+
.then(function (template) {
16+
response.send(template);
17+
logger.sendSuccessful({
18+
deleted: true,
19+
id: request.params.id
20+
});
21+
})
22+
.catch(function (error) {
23+
logger.sendFailed(error);
24+
sendRequestError(response, error);
25+
});
26+
})
27+
.catch(function (error) {
28+
logger.sendFailed(error);
29+
sendRequestError(response, error);
30+
});
31+
}

src/handlers/templates/id/get.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import RouteLogger from '../../../routes/route_logger';
2+
import {sendRequestError} from '../../../common/errors/utils';
3+
4+
let logger = new RouteLogger('/templates/:id');
5+
6+
export default function templateGetHandler(request, response) {
7+
/**
8+
* @type {ElastalertServer}
9+
*/
10+
let server = request.app.get('server');
11+
12+
server.templatesController.template(request.params.id)
13+
.then(function (template) {
14+
template.get()
15+
.then(function (template) {
16+
response.send(template);
17+
logger.sendSuccessful();
18+
})
19+
.catch(function (error) {
20+
logger.sendFailed(error);
21+
sendRequestError(response, error);
22+
});
23+
})
24+
.catch(function (error) {
25+
logger.sendFailed(error);
26+
sendRequestError(response, error);
27+
});
28+
}

0 commit comments

Comments
 (0)