Skip to content

Commit

Permalink
[Alerting] Adding feature flag for enabling/disabling rule import and…
Browse files Browse the repository at this point in the history
… export (#100718) (#100887)

* Adding feature flag for enabling rule import and export

* Removing item from docs

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: ymao1 <ying.mao@elastic.co>
  • Loading branch information
kibanamachine and ymao1 authored May 28, 2021
1 parent 3fe14f4 commit 41ef9ec
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 59 deletions.
9 changes: 0 additions & 9 deletions docs/user/alerting/rule-management.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,6 @@ These operations can also be performed in bulk by multi-selecting rules and clic
[role="screenshot"]
image:images/bulk-mute-disable.png[The Manage rules button lets you mute/unmute, enable/disable, and delete in bulk]

[float]
[[importing-and-exporting-rules]]
=== Importing and exporting rules

To import and export rules, use the <<managing-saved-objects, Saved Objects Management UI>>.
After the succesful import the proper banner will be displayed:
[role="screenshot"]
image::images/rules-imported-banner.png[Rules import banner, width=50%]

[float]
=== Required permissions

Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/alerting/server/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import { configSchema } from './config';

describe('config validation', () => {
test('alerts defaults', () => {
test('alerting defaults', () => {
const config: Record<string, unknown> = {};
expect(configSchema.validate(config)).toMatchInlineSnapshot(`
Object {
"enableImportExport": false,
"healthCheck": Object {
"interval": "60m",
},
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const configSchema = schema.object({
interval: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }),
removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '1h' }),
}),
enableImportExport: schema.boolean({ defaultValue: false }),
});

export type AlertsConfig = TypeOf<typeof configSchema>;
8 changes: 8 additions & 0 deletions x-pack/plugins/alerting/server/health/get_state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
}),
pollInterval
).subscribe();
Expand Down Expand Up @@ -107,6 +108,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
}),
pollInterval,
retryDelay
Expand Down Expand Up @@ -152,6 +154,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
})
).toPromise();

Expand Down Expand Up @@ -182,6 +185,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
})
).toPromise();

Expand Down Expand Up @@ -212,6 +216,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
})
).toPromise();

Expand Down Expand Up @@ -239,6 +244,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
}),
retryDelay
).subscribe((status) => {
Expand Down Expand Up @@ -269,6 +275,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
}),
retryDelay
).subscribe((status) => {
Expand Down Expand Up @@ -305,6 +312,7 @@ describe('getHealthServiceStatusWithRetryAndErrorHandling', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
})
).toPromise();

Expand Down
73 changes: 73 additions & 0 deletions x-pack/plugins/alerting/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ import { AlertsConfig } from './config';
import { AlertType } from './types';
import { eventLogMock } from '../../event_log/server/mocks';
import { actionsMock } from '../../actions/server/mocks';
import mappings from './saved_objects/mappings.json';

describe('Alerting Plugin', () => {
describe('setup()', () => {
let plugin: AlertingPlugin;
let coreSetup: ReturnType<typeof coreMock.createSetup>;
let pluginsSetup: jest.Mocked<AlertingPluginsSetup>;

beforeEach(() => jest.clearAllMocks());

it('should log warning when Encrypted Saved Objects plugin is missing encryption key', async () => {
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
healthCheck: {
Expand All @@ -34,6 +37,7 @@ describe('Alerting Plugin', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
});
plugin = new AlertingPlugin(context);

Expand All @@ -57,6 +61,72 @@ describe('Alerting Plugin', () => {
);
});

it('should register saved object with no management capability if enableImportExport is false', async () => {
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
healthCheck: {
interval: '5m',
},
invalidateApiKeysTask: {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
});
plugin = new AlertingPlugin(context);

const setupMocks = coreMock.createSetup();
await plugin.setup(setupMocks, {
licensing: licensingMock.createSetup(),
encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(),
taskManager: taskManagerMock.createSetup(),
eventLog: eventLogServiceMock.create(),
actions: actionsMock.createSetup(),
statusService: statusServiceMock.createSetupContract(),
});

expect(setupMocks.savedObjects.registerType).toHaveBeenCalledTimes(2);
const registerAlertingSavedObject = setupMocks.savedObjects.registerType.mock.calls[0][0];
expect(registerAlertingSavedObject.name).toEqual('alert');
expect(registerAlertingSavedObject.hidden).toBe(true);
expect(registerAlertingSavedObject.mappings).toEqual(mappings.alert);
expect(registerAlertingSavedObject.management).toBeUndefined();
});

it('should register saved object with import/export capability if enableImportExport is true', async () => {
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
healthCheck: {
interval: '5m',
},
invalidateApiKeysTask: {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: true,
});
plugin = new AlertingPlugin(context);

const setupMocks = coreMock.createSetup();
await plugin.setup(setupMocks, {
licensing: licensingMock.createSetup(),
encryptedSavedObjects: encryptedSavedObjectsMock.createSetup(),
taskManager: taskManagerMock.createSetup(),
eventLog: eventLogServiceMock.create(),
actions: actionsMock.createSetup(),
statusService: statusServiceMock.createSetupContract(),
});

expect(setupMocks.savedObjects.registerType).toHaveBeenCalledTimes(2);
const registerAlertingSavedObject = setupMocks.savedObjects.registerType.mock.calls[0][0];
expect(registerAlertingSavedObject.name).toEqual('alert');
expect(registerAlertingSavedObject.hidden).toBe(true);
expect(registerAlertingSavedObject.mappings).toEqual(mappings.alert);
expect(registerAlertingSavedObject.management).not.toBeUndefined();
expect(registerAlertingSavedObject.management?.importableAndExportable).toBe(true);
expect(registerAlertingSavedObject.management?.getTitle).not.toBeUndefined();
expect(registerAlertingSavedObject.management?.onImport).not.toBeUndefined();
expect(registerAlertingSavedObject.management?.onExport).not.toBeUndefined();
});

describe('registerType()', () => {
let setup: PluginSetupContract;
const sampleAlertType: AlertType<never, never, never, never, 'default'> = {
Expand Down Expand Up @@ -119,6 +189,7 @@ describe('Alerting Plugin', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
});
const plugin = new AlertingPlugin(context);

Expand Down Expand Up @@ -158,6 +229,7 @@ describe('Alerting Plugin', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
});
const plugin = new AlertingPlugin(context);

Expand Down Expand Up @@ -211,6 +283,7 @@ describe('Alerting Plugin', () => {
interval: '5m',
removalDelay: '1h',
},
enableImportExport: false,
});
const plugin = new AlertingPlugin(context);

Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/alerting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export class AlertingPlugin {
event: { provider: EVENT_LOG_PROVIDER },
});

setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects);
setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects, this.config);

this.eventLogService = plugins.eventLog;
plugins.eventLog.registerProviderActions(EVENT_LOG_PROVIDER, Object.values(EVENT_LOG_ACTIONS));
Expand Down
104 changes: 56 additions & 48 deletions x-pack/plugins/alerting/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objec
import { transformRulesForExport } from './transform_rule_for_export';
import { RawAlert } from '../types';
import { getImportWarnings } from './get_import_warnings';
import { AlertsConfig } from '../config';
export { partiallyUpdateAlert } from './partially_update_alert';

export const AlertAttributesExcludedFromAAD = [
Expand All @@ -41,59 +42,66 @@ export type AlertAttributesExcludedFromAADType =

export function setupSavedObjects(
savedObjects: SavedObjectsServiceSetup,
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
alertingConfig: Promise<AlertsConfig>
) {
savedObjects.registerType({
name: 'alert',
hidden: true,
namespaceType: 'single',
migrations: getMigrations(encryptedSavedObjects),
mappings: mappings.alert,
management: {
importableAndExportable: true,
getTitle(ruleSavedObject: SavedObject<RawAlert>) {
return `Rule: [${ruleSavedObject.attributes.name}]`;
},
onImport(ruleSavedObjects) {
return {
warnings: getImportWarnings(ruleSavedObjects),
};
},
onExport<RawAlert>(
context: SavedObjectsExportTransformContext,
objects: Array<SavedObject<RawAlert>>
) {
return transformRulesForExport(objects);
},
},
});
alertingConfig.then((config: AlertsConfig) => {
savedObjects.registerType({
name: 'alert',
hidden: true,
namespaceType: 'single',
migrations: getMigrations(encryptedSavedObjects),
mappings: mappings.alert,
...(config.enableImportExport
? {
management: {
importableAndExportable: true,
getTitle(ruleSavedObject: SavedObject<RawAlert>) {
return `Rule: [${ruleSavedObject.attributes.name}]`;
},
onImport(ruleSavedObjects) {
return {
warnings: getImportWarnings(ruleSavedObjects),
};
},
onExport<RawAlert>(
context: SavedObjectsExportTransformContext,
objects: Array<SavedObject<RawAlert>>
) {
return transformRulesForExport(objects);
},
},
}
: {}),
});

savedObjects.registerType({
name: 'api_key_pending_invalidation',
hidden: true,
namespaceType: 'agnostic',
mappings: {
properties: {
apiKeyId: {
type: 'keyword',
},
createdAt: {
type: 'date',
savedObjects.registerType({
name: 'api_key_pending_invalidation',
hidden: true,
namespaceType: 'agnostic',
mappings: {
properties: {
apiKeyId: {
type: 'keyword',
},
createdAt: {
type: 'date',
},
},
},
},
});
});

// Encrypted attributes
encryptedSavedObjects.registerType({
type: 'alert',
attributesToEncrypt: new Set(['apiKey']),
attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD),
});
// Encrypted attributes
encryptedSavedObjects.registerType({
type: 'alert',
attributesToEncrypt: new Set(['apiKey']),
attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD),
});

// Encrypted attributes
encryptedSavedObjects.registerType({
type: 'api_key_pending_invalidation',
attributesToEncrypt: new Set(['apiKeyId']),
// Encrypted attributes
encryptedSavedObjects.registerType({
type: 'api_key_pending_invalidation',
attributesToEncrypt: new Set(['apiKeyId']),
});
});
}

0 comments on commit 41ef9ec

Please sign in to comment.