Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions x-pack/plugins/beats/server/lib/index_template/beats_template.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@
}
}
},
"configuration_block": {
"tag": {
"properties": {
"tag": {
"type": "keyword"
},
"type": {
"id": {
"type": "keyword"
},
"block_yml": {
"type": "text"
"configuration_blocks": {
"type": "nested",
"properties": {
"type": {
"type": "keyword"
},
"block_yml": {
"type": "text"
}
}
}
}
},
Expand Down Expand Up @@ -69,6 +74,9 @@
"local_configuration_yml": {
"type": "text"
},
"tags": {
"type": "keyword"
},
"central_configuration_yml": {
"type": "text"
},
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/beats/server/routes/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import { registerEnrollBeatRoute } from './register_enroll_beat_route';
import { registerListBeatsRoute } from './register_list_beats_route';
import { registerVerifyBeatsRoute } from './register_verify_beats_route';
import { registerUpdateBeatRoute } from './register_update_beat_route';
import { registerSetTagRoute } from './register_set_tag_route';

export function registerApiRoutes(server) {
registerCreateEnrollmentTokensRoute(server);
registerEnrollBeatRoute(server);
registerListBeatsRoute(server);
registerVerifyBeatsRoute(server);
registerUpdateBeatRoute(server);
registerSetTagRoute(server);
}
124 changes: 124 additions & 0 deletions x-pack/plugins/beats/server/routes/api/register_set_tag_route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import Joi from 'joi';
import {
get,
uniq,
intersection
} from 'lodash';
import {
INDEX_NAMES,
CONFIGURATION_BLOCKS
} from '../../../common/constants';
import { callWithRequestFactory } from '../../lib/client';
import { wrapEsError } from '../../lib/error_wrappers';

function validateUniquenessEnforcingTypes(configurationBlocks) {
const types = uniq(configurationBlocks.map(block => block.type));

// If none of the types in the given configuration blocks are uniqueness-enforcing,
// we don't need to perform any further validation checks.
const uniquenessEnforcingTypes = intersection(types, CONFIGURATION_BLOCKS.UNIQUENESS_ENFORCING_TYPES);
if (uniquenessEnforcingTypes.length === 0) {
return { isValid: true };
}

// Count the number of uniqueness-enforcing types in the given configuration blocks
const typeCountMap = configurationBlocks.reduce((typeCountMap, block) => {
const { type } = block;
if (!uniquenessEnforcingTypes.includes(type)) {
return typeCountMap;
}

const count = typeCountMap[type] || 0;
return {
...typeCountMap,
[type]: count + 1
};
}, {});

// If there is no more than one of any uniqueness-enforcing types in the given
// configuration blocks, we don't need to perform any further validation checks.
if (Object.values(typeCountMap).filter(count => count > 1).length === 0) {
return { isValid: true };
}

const message = Object.entries(typeCountMap)
.filter(([, count]) => count > 1)
.map(([type, count]) => `Expected only one configuration block of type '${type}' but found ${count}`)
.join(' ');

return {
isValid: false,
message
};
}

async function validateConfigurationBlocks(configurationBlocks) {
return validateUniquenessEnforcingTypes(configurationBlocks);
}

async function persistTag(callWithRequest, tag) {
const body = {
type: 'tag',
tag
};

const params = {
index: INDEX_NAMES.BEATS,
type: '_doc',
id: `tag:${tag.id}`,
body,
refresh: 'wait_for'
};

const response = await callWithRequest('index', params);
return response.result;
}

// TODO: add license check pre-hook
// TODO: write to Kibana audit log file
export function registerSetTagRoute(server) {
server.route({
method: 'PUT',
path: '/api/beats/tag/{tag}',
config: {
validate: {
payload: Joi.object({
configuration_blocks: Joi.array().items(
Joi.object({
type: Joi.string().required().valid(Object.values(CONFIGURATION_BLOCKS.TYPES)),
block_yml: Joi.string().required()
})
)
}).allow(null)
}
},
handler: async (request, reply) => {
const callWithRequest = callWithRequestFactory(server, request);

let result;
try {
const configurationBlocks = get(request, 'payload.configuration_blocks', []);
const { isValid, message } = await validateConfigurationBlocks(configurationBlocks);
if (!isValid) {
return reply({ message }).code(400);
}

const tag = {
id: request.params.tag,
configuration_blocks: configurationBlocks
};
result = await persistTag(callWithRequest, tag);
} catch (err) {
return reply(wrapEsError(err));
}

reply().code(result === 'created' ? 201 : 200);
}
});
}
1 change: 1 addition & 0 deletions x-pack/test/api_integration/apis/beats/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export default function ({ getService, loadTestFile }) {
loadTestFile(require.resolve('./list_beats'));
loadTestFile(require.resolve('./verify_beats'));
loadTestFile(require.resolve('./update_beat'));
loadTestFile(require.resolve('./set_tag'));
});
}
207 changes: 207 additions & 0 deletions x-pack/test/api_integration/apis/beats/set_tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import expect from 'expect.js';
import {
ES_INDEX_NAME,
ES_TYPE_NAME
} from './constants';

export default function ({ getService }) {
const supertest = getService('supertest');
const chance = getService('chance');
const es = getService('es');

describe('set_tag', () => {
it('should create an empty tag', async () => {
const tagId = 'production';
await supertest
.put(
`/api/beats/tag/${tagId}`
)
.set('kbn-xsrf', 'xxx')
.send()
.expect(201);

const esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `tag:${tagId}`
});

const tagInEs = esResponse._source;

expect(tagInEs.type).to.be('tag');
expect(tagInEs.tag.id).to.be(tagId);
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
expect(tagInEs.tag.configuration_blocks.length).to.be(0);
});

it('should create a tag with one configuration block', async () => {
const tagId = 'production';
await supertest
.put(
`/api/beats/tag/${tagId}`
)
.set('kbn-xsrf', 'xxx')
.send({
configuration_blocks: [
{
type: 'output',
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
}
]
})
.expect(201);

const esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `tag:${tagId}`
});

const tagInEs = esResponse._source;

expect(tagInEs.type).to.be('tag');
expect(tagInEs.tag.id).to.be(tagId);
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
expect(tagInEs.tag.configuration_blocks.length).to.be(1);
expect(tagInEs.tag.configuration_blocks[0].type).to.be('output');
expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."');
});

it('should create a tag with two configuration blocks', async () => {
const tagId = 'production';
await supertest
.put(
`/api/beats/tag/${tagId}`
)
.set('kbn-xsrf', 'xxx')
.send({
configuration_blocks: [
{
type: 'filebeat.inputs',
block_yml: 'file:\n path: "/var/log/some.log"]\n'
},
{
type: 'output',
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
}
]
})
.expect(201);

const esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `tag:${tagId}`
});

const tagInEs = esResponse._source;

expect(tagInEs.type).to.be('tag');
expect(tagInEs.tag.id).to.be(tagId);
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
expect(tagInEs.tag.configuration_blocks.length).to.be(2);
expect(tagInEs.tag.configuration_blocks[0].type).to.be('filebeat.inputs');
expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('file:\n path: "/var/log/some.log"]\n');
expect(tagInEs.tag.configuration_blocks[1].type).to.be('output');
expect(tagInEs.tag.configuration_blocks[1].block_yml).to.be('elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."');
});

it('should fail when creating a tag with two configuration blocks of type output', async () => {
const tagId = 'production';
await supertest
.put(
`/api/beats/tag/${tagId}`
)
.set('kbn-xsrf', 'xxx')
.send({
configuration_blocks: [
{
type: 'output',
block_yml: 'logstash:\n hosts: ["localhost:9000"]\n'
},
{
type: 'output',
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
}
]
})
.expect(400);
});

it('should fail when creating a tag with an invalid configuration block type', async () => {
const tagId = 'production';
await supertest
.put(
`/api/beats/tag/${tagId}`
)
.set('kbn-xsrf', 'xxx')
.send({
configuration_blocks: [
{
type: chance.word(),
block_yml: 'logstash:\n hosts: ["localhost:9000"]\n'
}
]
})
.expect(400);
});

it('should update an existing tag', async () => {
const tagId = 'production';
await supertest
.put(
`/api/beats/tag/${tagId}`
)
.set('kbn-xsrf', 'xxx')
.send({
configuration_blocks: [
{
type: 'filebeat.inputs',
block_yml: 'file:\n path: "/var/log/some.log"]\n'
},
{
type: 'output',
block_yml: 'elasticsearch:\n hosts: [\"localhost:9200\"]\n username: "..."'
}
]
})
.expect(201);

await supertest
.put(
`/api/beats/tag/${tagId}`
)
.set('kbn-xsrf', 'xxx')
.send({
configuration_blocks: [
{
type: 'output',
block_yml: 'logstash:\n hosts: ["localhost:9000"]\n'
}
]
})
.expect(200);

const esResponse = await es.get({
index: ES_INDEX_NAME,
type: ES_TYPE_NAME,
id: `tag:${tagId}`
});

const tagInEs = esResponse._source;

expect(tagInEs.type).to.be('tag');
expect(tagInEs.tag.id).to.be(tagId);
expect(tagInEs.tag.configuration_blocks).to.be.an(Array);
expect(tagInEs.tag.configuration_blocks.length).to.be(1);
expect(tagInEs.tag.configuration_blocks[0].type).to.be('output');
expect(tagInEs.tag.configuration_blocks[0].block_yml).to.be('logstash:\n hosts: ["localhost:9000"]\n');
});
});
}
Loading