Skip to content

Commit 710e7c7

Browse files
ycombinatormattapperson
authored andcommitted
[Beats Management] APIs: Create or update tag (#19342)
* Updating mappings * Implementing PUT /api/beats/tag/{tag} API
1 parent 1bcf5ba commit 710e7c7

File tree

6 files changed

+364
-14
lines changed

6 files changed

+364
-14
lines changed

x-pack/plugins/beats/server/lib/index_template/beats_template.json

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,21 @@
2727
}
2828
}
2929
},
30-
"configuration_block": {
30+
"tag": {
3131
"properties": {
32-
"tag": {
33-
"type": "keyword"
34-
},
35-
"type": {
32+
"id": {
3633
"type": "keyword"
3734
},
38-
"block_yml": {
39-
"type": "text"
35+
"configuration_blocks": {
36+
"type": "nested",
37+
"properties": {
38+
"type": {
39+
"type": "keyword"
40+
},
41+
"block_yml": {
42+
"type": "text"
43+
}
44+
}
4045
}
4146
}
4247
},
@@ -69,6 +74,9 @@
6974
"local_configuration_yml": {
7075
"type": "text"
7176
},
77+
"tags": {
78+
"type": "keyword"
79+
},
7280
"central_configuration_yml": {
7381
"type": "text"
7482
},

x-pack/plugins/beats/server/routes/api/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import { registerEnrollBeatRoute } from './register_enroll_beat_route';
99
import { registerListBeatsRoute } from './register_list_beats_route';
1010
import { registerVerifyBeatsRoute } from './register_verify_beats_route';
1111
import { registerUpdateBeatRoute } from './register_update_beat_route';
12+
import { registerSetTagRoute } from './register_set_tag_route';
1213

1314
export function registerApiRoutes(server) {
1415
registerCreateEnrollmentTokensRoute(server);
1516
registerEnrollBeatRoute(server);
1617
registerListBeatsRoute(server);
1718
registerVerifyBeatsRoute(server);
1819
registerUpdateBeatRoute(server);
20+
registerSetTagRoute(server);
1921
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import Joi from 'joi';
8+
import {
9+
get,
10+
uniq,
11+
intersection
12+
} from 'lodash';
13+
import {
14+
INDEX_NAMES,
15+
CONFIGURATION_BLOCKS
16+
} from '../../../common/constants';
17+
import { callWithRequestFactory } from '../../lib/client';
18+
import { wrapEsError } from '../../lib/error_wrappers';
19+
20+
function validateUniquenessEnforcingTypes(configurationBlocks) {
21+
const types = uniq(configurationBlocks.map(block => block.type));
22+
23+
// If none of the types in the given configuration blocks are uniqueness-enforcing,
24+
// we don't need to perform any further validation checks.
25+
const uniquenessEnforcingTypes = intersection(types, CONFIGURATION_BLOCKS.UNIQUENESS_ENFORCING_TYPES);
26+
if (uniquenessEnforcingTypes.length === 0) {
27+
return { isValid: true };
28+
}
29+
30+
// Count the number of uniqueness-enforcing types in the given configuration blocks
31+
const typeCountMap = configurationBlocks.reduce((typeCountMap, block) => {
32+
const { type } = block;
33+
if (!uniquenessEnforcingTypes.includes(type)) {
34+
return typeCountMap;
35+
}
36+
37+
const count = typeCountMap[type] || 0;
38+
return {
39+
...typeCountMap,
40+
[type]: count + 1
41+
};
42+
}, {});
43+
44+
// If there is no more than one of any uniqueness-enforcing types in the given
45+
// configuration blocks, we don't need to perform any further validation checks.
46+
if (Object.values(typeCountMap).filter(count => count > 1).length === 0) {
47+
return { isValid: true };
48+
}
49+
50+
const message = Object.entries(typeCountMap)
51+
.filter(([, count]) => count > 1)
52+
.map(([type, count]) => `Expected only one configuration block of type '${type}' but found ${count}`)
53+
.join(' ');
54+
55+
return {
56+
isValid: false,
57+
message
58+
};
59+
}
60+
61+
async function validateConfigurationBlocks(configurationBlocks) {
62+
return validateUniquenessEnforcingTypes(configurationBlocks);
63+
}
64+
65+
async function persistTag(callWithRequest, tag) {
66+
const body = {
67+
type: 'tag',
68+
tag
69+
};
70+
71+
const params = {
72+
index: INDEX_NAMES.BEATS,
73+
type: '_doc',
74+
id: `tag:${tag.id}`,
75+
body,
76+
refresh: 'wait_for'
77+
};
78+
79+
const response = await callWithRequest('index', params);
80+
return response.result;
81+
}
82+
83+
// TODO: add license check pre-hook
84+
// TODO: write to Kibana audit log file
85+
export function registerSetTagRoute(server) {
86+
server.route({
87+
method: 'PUT',
88+
path: '/api/beats/tag/{tag}',
89+
config: {
90+
validate: {
91+
payload: Joi.object({
92+
configuration_blocks: Joi.array().items(
93+
Joi.object({
94+
type: Joi.string().required().valid(Object.values(CONFIGURATION_BLOCKS.TYPES)),
95+
block_yml: Joi.string().required()
96+
})
97+
)
98+
}).allow(null)
99+
}
100+
},
101+
handler: async (request, reply) => {
102+
const callWithRequest = callWithRequestFactory(server, request);
103+
104+
let result;
105+
try {
106+
const configurationBlocks = get(request, 'payload.configuration_blocks', []);
107+
const { isValid, message } = await validateConfigurationBlocks(configurationBlocks);
108+
if (!isValid) {
109+
return reply({ message }).code(400);
110+
}
111+
112+
const tag = {
113+
id: request.params.tag,
114+
configuration_blocks: configurationBlocks
115+
};
116+
result = await persistTag(callWithRequest, tag);
117+
} catch (err) {
118+
return reply(wrapEsError(err));
119+
}
120+
121+
reply().code(result === 'created' ? 201 : 200);
122+
}
123+
});
124+
}

x-pack/test/api_integration/apis/beats/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ export default function ({ getService, loadTestFile }) {
2222
loadTestFile(require.resolve('./list_beats'));
2323
loadTestFile(require.resolve('./verify_beats'));
2424
loadTestFile(require.resolve('./update_beat'));
25+
loadTestFile(require.resolve('./set_tag'));
2526
});
2627
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import expect from 'expect.js';
8+
import {
9+
ES_INDEX_NAME,
10+
ES_TYPE_NAME
11+
} from './constants';
12+
13+
export default function ({ getService }) {
14+
const supertest = getService('supertest');
15+
const chance = getService('chance');
16+
const es = getService('es');
17+
18+
describe('set_tag', () => {
19+
it('should create an empty tag', async () => {
20+
const tagId = 'production';
21+
await supertest
22+
.put(
23+
`/api/beats/tag/${tagId}`
24+
)
25+
.set('kbn-xsrf', 'xxx')
26+
.send()
27+
.expect(201);
28+
29+
const esResponse = await es.get({
30+
index: ES_INDEX_NAME,
31+
type: ES_TYPE_NAME,
32+
id: `tag:${tagId}`
33+
});
34+
35+
const tagInEs = esResponse._source;
36+
37+
expect(tagInEs.type).to.be('tag');
38+
expect(tagInEs.tag.id).to.be(tagId);
39+
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
40+
expect(tagInEs.tag.configuration_blocks.length).to.be(0);
41+
});
42+
43+
it('should create a tag with one configuration block', async () => {
44+
const tagId = 'production';
45+
await supertest
46+
.put(
47+
`/api/beats/tag/${tagId}`
48+
)
49+
.set('kbn-xsrf', 'xxx')
50+
.send({
51+
configuration_blocks: [
52+
{
53+
type: 'output',
54+
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
55+
}
56+
]
57+
})
58+
.expect(201);
59+
60+
const esResponse = await es.get({
61+
index: ES_INDEX_NAME,
62+
type: ES_TYPE_NAME,
63+
id: `tag:${tagId}`
64+
});
65+
66+
const tagInEs = esResponse._source;
67+
68+
expect(tagInEs.type).to.be('tag');
69+
expect(tagInEs.tag.id).to.be(tagId);
70+
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
71+
expect(tagInEs.tag.configuration_blocks.length).to.be(1);
72+
expect(tagInEs.tag.configuration_blocks[0].type).to.be('output');
73+
expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."');
74+
});
75+
76+
it('should create a tag with two configuration blocks', async () => {
77+
const tagId = 'production';
78+
await supertest
79+
.put(
80+
`/api/beats/tag/${tagId}`
81+
)
82+
.set('kbn-xsrf', 'xxx')
83+
.send({
84+
configuration_blocks: [
85+
{
86+
type: 'filebeat.inputs',
87+
block_yml: 'file:\n path: "/var/log/some.log"]\n'
88+
},
89+
{
90+
type: 'output',
91+
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
92+
}
93+
]
94+
})
95+
.expect(201);
96+
97+
const esResponse = await es.get({
98+
index: ES_INDEX_NAME,
99+
type: ES_TYPE_NAME,
100+
id: `tag:${tagId}`
101+
});
102+
103+
const tagInEs = esResponse._source;
104+
105+
expect(tagInEs.type).to.be('tag');
106+
expect(tagInEs.tag.id).to.be(tagId);
107+
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
108+
expect(tagInEs.tag.configuration_blocks.length).to.be(2);
109+
expect(tagInEs.tag.configuration_blocks[0].type).to.be('filebeat.inputs');
110+
expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('file:\n path: "/var/log/some.log"]\n');
111+
expect(tagInEs.tag.configuration_blocks[1].type).to.be('output');
112+
expect(tagInEs.tag.configuration_blocks[1].block_yml).to.be('elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."');
113+
});
114+
115+
it('should fail when creating a tag with two configuration blocks of type output', async () => {
116+
const tagId = 'production';
117+
await supertest
118+
.put(
119+
`/api/beats/tag/${tagId}`
120+
)
121+
.set('kbn-xsrf', 'xxx')
122+
.send({
123+
configuration_blocks: [
124+
{
125+
type: 'output',
126+
block_yml: 'logstash:\n hosts: ["localhost:9000"]\n'
127+
},
128+
{
129+
type: 'output',
130+
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
131+
}
132+
]
133+
})
134+
.expect(400);
135+
});
136+
137+
it('should fail when creating a tag with an invalid configuration block type', async () => {
138+
const tagId = 'production';
139+
await supertest
140+
.put(
141+
`/api/beats/tag/${tagId}`
142+
)
143+
.set('kbn-xsrf', 'xxx')
144+
.send({
145+
configuration_blocks: [
146+
{
147+
type: chance.word(),
148+
block_yml: 'logstash:\n hosts: ["localhost:9000"]\n'
149+
}
150+
]
151+
})
152+
.expect(400);
153+
});
154+
155+
it('should update an existing tag', async () => {
156+
const tagId = 'production';
157+
await supertest
158+
.put(
159+
`/api/beats/tag/${tagId}`
160+
)
161+
.set('kbn-xsrf', 'xxx')
162+
.send({
163+
configuration_blocks: [
164+
{
165+
type: 'filebeat.inputs',
166+
block_yml: 'file:\n path: "/var/log/some.log"]\n'
167+
},
168+
{
169+
type: 'output',
170+
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
171+
}
172+
]
173+
})
174+
.expect(201);
175+
176+
await supertest
177+
.put(
178+
`/api/beats/tag/${tagId}`
179+
)
180+
.set('kbn-xsrf', 'xxx')
181+
.send({
182+
configuration_blocks: [
183+
{
184+
type: 'output',
185+
block_yml: 'logstash:\n hosts: ["localhost:9000"]\n'
186+
}
187+
]
188+
})
189+
.expect(200);
190+
191+
const esResponse = await es.get({
192+
index: ES_INDEX_NAME,
193+
type: ES_TYPE_NAME,
194+
id: `tag:${tagId}`
195+
});
196+
197+
const tagInEs = esResponse._source;
198+
199+
expect(tagInEs.type).to.be('tag');
200+
expect(tagInEs.tag.id).to.be(tagId);
201+
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
202+
expect(tagInEs.tag.configuration_blocks.length).to.be(1);
203+
expect(tagInEs.tag.configuration_blocks[0].type).to.be('output');
204+
expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('logstash:\n hosts: ["localhost:9000"]\n');
205+
});
206+
});
207+
}

0 commit comments

Comments
 (0)