Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RAC] Rule monitoring: Event Log for Rule Registry #98353

Merged
merged 6 commits into from
May 27, 2021
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
19 changes: 9 additions & 10 deletions x-pack/plugins/apm/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,19 +130,20 @@ export class APMPlugin

registerFeaturesUsage({ licensingPlugin: plugins.licensing });

const { ruleDataService } = plugins.ruleRegistry;
const getCoreStart = () =>
core.getStartServices().then(([coreStart]) => coreStart);

const ready = once(async () => {
const componentTemplateName = plugins.ruleRegistry.getFullAssetName(
const componentTemplateName = ruleDataService.getFullAssetName(
'apm-mappings'
);

if (!plugins.ruleRegistry.isWriteEnabled()) {
if (!ruleDataService.isWriteEnabled()) {
return;
}

await plugins.ruleRegistry.createOrUpdateComponentTemplate({
await ruleDataService.createOrUpdateComponentTemplate({
name: componentTemplateName,
body: {
template: {
Expand All @@ -167,16 +168,14 @@ export class APMPlugin
},
});

await plugins.ruleRegistry.createOrUpdateIndexTemplate({
name: plugins.ruleRegistry.getFullAssetName('apm-index-template'),
await ruleDataService.createOrUpdateIndexTemplate({
name: ruleDataService.getFullAssetName('apm-index-template'),
body: {
index_patterns: [
plugins.ruleRegistry.getFullAssetName('observability-apm*'),
ruleDataService.getFullAssetName('observability-apm*'),
],
composed_of: [
plugins.ruleRegistry.getFullAssetName(
TECHNICAL_COMPONENT_TEMPLATE_NAME
),
ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
componentTemplateName,
],
},
Expand All @@ -188,7 +187,7 @@ export class APMPlugin
});

const ruleDataClient = new RuleDataClient({
alias: plugins.ruleRegistry.getFullAssetName('observability-apm'),
alias: ruleDataService.getFullAssetName('observability-apm'),
getClusterClient: async () => {
const coreStart = await getCoreStart();
return coreStart.elasticsearch.client.asInternalUser;
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/observability/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
return coreStart.elasticsearch.client.asInternalUser;
},
ready: () => Promise.resolve(),
alias: plugins.ruleRegistry.getFullAssetName(),
alias: plugins.ruleRegistry.ruleDataService.getFullAssetName(),
});

registerRoutes({
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/rule_registry/kibana.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
],
"requiredPlugins": [
"alerting",
"data",
"spaces",
"triggersActionsUi"
],
"server": true
Expand Down
20 changes: 20 additions & 0 deletions x-pack/plugins/rule_registry/server/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { schema, TypeOf } from '@kbn/config-schema';

export const config = {
schema: schema.object({
enabled: schema.boolean({ defaultValue: true }),
write: schema.object({
enabled: schema.boolean({ defaultValue: true }),
}),
index: schema.string({ defaultValue: '.alerts' }),
}),
};

export type RuleRegistryPluginConfig = TypeOf<typeof config.schema>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

export * from './index_bootstrapper';
export * from './index_management_gateway';
export * from './index_reader';
export * from './index_writer';
export * from './resources/ilm_policy';
export * from './resources/index_mappings';
export * from './resources/index_names';
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { PublicMethodsOf } from '@kbn/utility-types';
import { Logger } from 'src/core/server';

import { IndexNames } from './resources/index_names';
import { IndexMappings } from './resources/index_mappings';
import { createIndexTemplate } from './resources/index_template';
import { IlmPolicy, defaultIlmPolicy } from './resources/ilm_policy';
import { IIndexManagementGateway } from './index_management_gateway';

interface ConstructorParams {
gateway: IIndexManagementGateway;
logger: Logger;
}

export interface IndexSpecification {
indexNames: IndexNames;
indexMappings: IndexMappings;
ilmPolicy?: IlmPolicy;
}

export type IIndexBootstrapper = PublicMethodsOf<IndexBootstrapper>;

// TODO: Converge with the logic of .siem-signals index bootstrapping
// x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts

// TODO: Handle race conditions and potential errors between multiple instances of Kibana
// trying to bootstrap the same index. Possible options:
// - robust idempotent logic with error handling
// - leveraging task_manager to make sure bootstrapping is run only once at a time
// - using some sort of distributed lock
// Maybe we can check how Saved Objects service bootstraps .kibana index

export class IndexBootstrapper {
private readonly gateway: IIndexManagementGateway;
private readonly logger: Logger;

constructor(params: ConstructorParams) {
this.gateway = params.gateway;
this.logger = params.logger.get('IndexBootstrapper');
}

public async run(indexSpec: IndexSpecification): Promise<boolean> {
this.logger.debug('bootstrapping elasticsearch resources starting');

try {
const { indexNames, indexMappings, ilmPolicy } = indexSpec;
await this.createIlmPolicyIfNotExists(indexNames, ilmPolicy);
await this.createIndexTemplateIfNotExists(indexNames, indexMappings);
await this.createInitialIndexIfNotExists(indexNames);
} catch (err) {
this.logger.error(`error bootstrapping elasticsearch resources: ${err.message}`);
return false;
}

this.logger.debug('bootstrapping elasticsearch resources complete');
return true;
}

private async createIlmPolicyIfNotExists(names: IndexNames, policy?: IlmPolicy): Promise<void> {
const { indexIlmPolicyName } = names;

const exists = await this.gateway.doesIlmPolicyExist(indexIlmPolicyName);
if (!exists) {
const ilmPolicy = policy ?? defaultIlmPolicy;
await this.gateway.createIlmPolicy(indexIlmPolicyName, ilmPolicy);
}
}

private async createIndexTemplateIfNotExists(
names: IndexNames,
mappings: IndexMappings
): Promise<void> {
const { indexTemplateName } = names;

const templateVersion = 1; // TODO: get from EventSchema definition
const template = createIndexTemplate(names, mappings, templateVersion);

const exists = await this.gateway.doesIndexTemplateExist(indexTemplateName);
if (!exists) {
await this.gateway.createIndexTemplate(indexTemplateName, template);
} else {
await this.gateway.updateIndexTemplate(indexTemplateName, template);
}
}

private async createInitialIndexIfNotExists(names: IndexNames): Promise<void> {
const { indexAliasName, indexInitialName } = names;

const exists = await this.gateway.doesAliasExist(indexAliasName);
if (!exists) {
await this.gateway.createIndex(indexInitialName, {
aliases: {
[indexAliasName]: {
is_write_index: true,
},
},
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import type { PublicMethodsOf } from '@kbn/utility-types';
import { ElasticsearchClient, Logger } from 'src/core/server';
import { IlmPolicy } from './resources/ilm_policy';
import { IndexTemplate } from './resources/index_template';

interface ConstructorParams {
elasticsearch: Promise<ElasticsearchClient>;
logger: Logger;
}

export type IIndexManagementGateway = PublicMethodsOf<IndexManagementGateway>;

export class IndexManagementGateway {
private readonly elasticsearch: Promise<ElasticsearchClient>;
private readonly logger: Logger;

constructor(params: ConstructorParams) {
this.elasticsearch = params.elasticsearch;
this.logger = params.logger.get('IndexManagementGateway');
}

public async doesIlmPolicyExist(policyName: string): Promise<boolean> {
this.logger.debug(`Checking if ILM policy exists; name="${policyName}"`);

try {
const es = await this.elasticsearch;
await es.transport.request({
method: 'GET',
path: `/_ilm/policy/${policyName}`,
});
} catch (e) {
if (e.statusCode === 404) return false;
throw new Error(`Error checking existence of ILM policy: ${e.message}`);
}
return true;
}

public async createIlmPolicy(policyName: string, policy: IlmPolicy): Promise<void> {
this.logger.debug(`Creating ILM policy; name="${policyName}"`);

try {
const es = await this.elasticsearch;
await es.transport.request({
method: 'PUT',
path: `/_ilm/policy/${policyName}`,
body: policy,
});
} catch (e) {
throw new Error(`Error creating ILM policy: ${e.message}`);
}
}

public async doesIndexTemplateExist(templateName: string): Promise<boolean> {
this.logger.debug(`Checking if index template exists; name="${templateName}"`);

try {
const es = await this.elasticsearch;
const { body } = await es.indices.existsTemplate({ name: templateName });
return body as boolean;
} catch (e) {
throw new Error(`Error checking existence of index template: ${e.message}`);
}
}

public async createIndexTemplate(templateName: string, template: IndexTemplate): Promise<void> {
this.logger.debug(`Creating index template; name="${templateName}"`);

try {
const es = await this.elasticsearch;
await es.indices.putTemplate({ create: true, name: templateName, body: template });
} catch (e) {
// The error message doesn't have a type attribute we can look to guarantee it's due
// to the template already existing (only long message) so we'll check ourselves to see
// if the template now exists. This scenario would happen if you startup multiple Kibana
// instances at the same time.
const existsNow = await this.doesIndexTemplateExist(templateName);
if (!existsNow) {
const error = new Error(`Error creating index template: ${e.message}`);
Object.assign(error, { wrapped: e });
throw error;
}
}
}

public async updateIndexTemplate(templateName: string, template: IndexTemplate): Promise<void> {
this.logger.debug(`Updating index template; name="${templateName}"`);

try {
const { settings, ...templateWithoutSettings } = template;

const es = await this.elasticsearch;
await es.indices.putTemplate({
create: false,
name: templateName,
body: templateWithoutSettings,
});
} catch (e) {
throw new Error(`Error updating index template: ${e.message}`);
}
}

public async doesAliasExist(aliasName: string): Promise<boolean> {
this.logger.debug(`Checking if index alias exists; name="${aliasName}"`);

try {
const es = await this.elasticsearch;
const { body } = await es.indices.existsAlias({ name: aliasName });
return body as boolean;
} catch (e) {
throw new Error(`Error checking existence of initial index: ${e.message}`);
}
}

public async createIndex(indexName: string, body: Record<string, unknown> = {}): Promise<void> {
this.logger.debug(`Creating index; name="${indexName}"`);
this.logger.debug(JSON.stringify(body, null, 2));

try {
const es = await this.elasticsearch;
await es.indices.create({
index: indexName,
body,
});
} catch (e) {
if (e.body?.error?.type !== 'resource_already_exists_exception') {
this.logger.error(e);
this.logger.error(JSON.stringify(e.meta, null, 2));
throw new Error(`Error creating initial index: ${e.message}`);
}
}
}
}
Loading