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

Adding fallback migration function for rule and connector migrations #105820

Closed
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
153 changes: 87 additions & 66 deletions x-pack/plugins/actions/server/saved_objects/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,124 +13,145 @@ import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/serv
import { migrationMocks } from 'src/core/server/mocks';

const context = migrationMocks.createContext();
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
const encryptedSavedObjectsSetupNoError = encryptedSavedObjectsMock.createSetup();
const encryptedSavedObjectsSetupThrowsError = encryptedSavedObjectsMock.createSetup();

describe('7.10.0', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(
(shouldMigrateWhenPredicate, migration) => migration
);
beforeAll(() => {
encryptedSavedObjectsSetupNoError.createMigration.mockImplementation((_, migration) => migration);
encryptedSavedObjectsSetupThrowsError.createMigration.mockImplementation(() => () => {
throw new Error(`Can't migrate!`);
});
});

function testMigrationWhenNoEsoErrors(
connector: SavedObjectUnsanitizedDoc<Partial<RawAction>>,
expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>>,
version: string
) {
// should migrate correctly when no decrypt errors
expect(
getMigrations(encryptedSavedObjectsSetupNoError)[version](connector, context)
).toMatchObject(expectedMigratedConnector);
}

function testMigrationWhenEsoThrowsError(
connector: SavedObjectUnsanitizedDoc<Partial<RawAction>>,
expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>>,
version: string
) {
// should log error when decryption throws error but migrated correctly
expect(
getMigrations(encryptedSavedObjectsSetupThrowsError)[version](connector, context)
).toMatchObject(expectedMigratedConnector);
expect(context.log.error).toHaveBeenCalledWith(
`encryptedSavedObject ${version} migration failed for connector ${connector.id} with error: Can't migrate!`,
{
migrations: {
connectorDocument: connector,
},
}
);
}

describe('7.10.0', () => {
test('add hasAuth config property for .email actions', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const action = getMockDataForEmail({});
const migratedAction = migration710(action, context);
expect(migratedAction.attributes.config).toEqual({
hasAuth: true,
});
expect(migratedAction).toEqual({
...action,
const connector = getMockDataForEmail({});
const expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>> = {
...connector,
attributes: {
...action.attributes,
...connector.attributes,
config: {
hasAuth: true,
},
},
});
};

testMigrationWhenNoEsoErrors(connector, expectedMigratedConnector, '7.10.0');
testMigrationWhenEsoThrowsError(connector, expectedMigratedConnector, '7.10.0');
});

test('rename cases configuration object', () => {
const migration710 = getMigrations(encryptedSavedObjectsSetup)['7.10.0'];
const action = getCasesMockData({});
const migratedAction = migration710(action, context);
expect(migratedAction.attributes.config).toEqual({
incidentConfiguration: { mapping: [] },
});
expect(migratedAction).toEqual({
...action,
const connector = getCasesMockData({});
const expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>> = {
...connector,
attributes: {
...action.attributes,
...connector.attributes,
config: {
incidentConfiguration: { mapping: [] },
},
},
});
};

testMigrationWhenNoEsoErrors(connector, expectedMigratedConnector, '7.10.0');
testMigrationWhenEsoThrowsError(connector, expectedMigratedConnector, '7.10.0');
});
});

describe('7.11.0', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(
(shouldMigrateWhenPredicate, migration) => migration
);
});

test('add hasAuth = true for .webhook actions with user and password', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const action = getMockDataForWebhook({}, true);
expect(migration711(action, context)).toMatchObject({
...action,
const connector = getMockDataForWebhook({}, true);
const expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>> = {
...connector,
attributes: {
...action.attributes,
...connector.attributes,
config: {
hasAuth: true,
},
},
});
};

testMigrationWhenNoEsoErrors(connector, expectedMigratedConnector, '7.11.0');
testMigrationWhenEsoThrowsError(connector, expectedMigratedConnector, '7.11.0');
});

test('add hasAuth = false for .webhook actions without user and password', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const action = getMockDataForWebhook({}, false);
expect(migration711(action, context)).toMatchObject({
...action,
const connector = getMockDataForWebhook({}, false);
const expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>> = {
...connector,
attributes: {
...action.attributes,
...connector.attributes,
config: {
hasAuth: false,
},
},
});
};

testMigrationWhenNoEsoErrors(connector, expectedMigratedConnector, '7.11.0');
testMigrationWhenEsoThrowsError(connector, expectedMigratedConnector, '7.11.0');
});

test('remove cases mapping object', () => {
const migration711 = getMigrations(encryptedSavedObjectsSetup)['7.11.0'];
const action = getMockData({
const connector = getMockData({
config: { incidentConfiguration: { mapping: [] }, isCaseOwned: true, another: 'value' },
});
expect(migration711(action, context)).toEqual({
...action,
const expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>> = {
...connector,
attributes: {
...action.attributes,
...connector.attributes,
config: {
another: 'value',
},
},
});
};

testMigrationWhenNoEsoErrors(connector, expectedMigratedConnector, '7.11.0');
testMigrationWhenEsoThrowsError(connector, expectedMigratedConnector, '7.11.0');
});
});

describe('7.14.0', () => {
beforeEach(() => {
jest.resetAllMocks();
encryptedSavedObjectsSetup.createMigration.mockImplementation(
(shouldMigrateWhenPredicate, migration) => migration
);
});

test('add isMissingSecrets property for actions', () => {
const migration714 = getMigrations(encryptedSavedObjectsSetup)['7.14.0'];
const action = getMockData({ isMissingSecrets: undefined });
const migratedAction = migration714(action, context);
expect(migratedAction).toEqual({
...action,
const connector = getMockData({ isMissingSecrets: undefined });
const expectedMigratedConnector: SavedObjectUnsanitizedDoc<Partial<RawAction>> = {
...connector,
attributes: {
...action.attributes,
...connector.attributes,
isMissingSecrets: false,
},
});
};

testMigrationWhenNoEsoErrors(connector, expectedMigratedConnector, '7.14.0');
testMigrationWhenEsoThrowsError(connector, expectedMigratedConnector, '7.14.0');
});
});

Expand Down
91 changes: 63 additions & 28 deletions x-pack/plugins/actions/server/saved_objects/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,49 @@ import {
import { RawAction } from '../types';
import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server';

interface ActionsLogMeta extends LogMeta {
migrations: { actionDocument: SavedObjectUnsanitizedDoc<RawAction> };
// Remove this when we finish updating terminology in the code
type RawConnector = RawAction;

interface ConnectorLogMeta extends LogMeta {
migrations: { connectorDocument: SavedObjectUnsanitizedDoc<RawConnector> };
}

type ActionMigration = (
doc: SavedObjectUnsanitizedDoc<RawAction>
) => SavedObjectUnsanitizedDoc<RawAction>;
type ConnectorMigration = (
doc: SavedObjectUnsanitizedDoc<RawConnector>
) => SavedObjectUnsanitizedDoc<RawConnector>;

type IsMigrationNeededPredicate<InputAttributes> = (
doc: SavedObjectUnsanitizedDoc<InputAttributes>
) => doc is SavedObjectUnsanitizedDoc<InputAttributes>;

interface ConnectorMigrationFns<InputAttributes, MigratedAttributes> {
esoMigrationFn: SavedObjectMigrationFn<InputAttributes, MigratedAttributes>;
fallbackMigrationFn: SavedObjectMigrationFn<InputAttributes, MigratedAttributes>;
}

export function getMigrations(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
): SavedObjectMigrationMap {
const migrationActionsTen = encryptedSavedObjects.createMigration<RawAction, RawAction>(
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> =>
const migrationActionsTen = createMigrationFns(
encryptedSavedObjects,
(doc): doc is SavedObjectUnsanitizedDoc<RawConnector> =>
doc.attributes.config?.hasOwnProperty('casesConfiguration') ||
doc.attributes.actionTypeId === '.email',
pipeMigrations(renameCasesConfigurationObject, addHasAuthConfigurationObject)
);

const migrationActionsEleven = encryptedSavedObjects.createMigration<RawAction, RawAction>(
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> =>
const migrationActionsEleven = createMigrationFns(
encryptedSavedObjects,
(doc): doc is SavedObjectUnsanitizedDoc<RawConnector> =>
doc.attributes.config?.hasOwnProperty('isCaseOwned') ||
doc.attributes.config?.hasOwnProperty('incidentConfiguration') ||
doc.attributes.actionTypeId === '.webhook',
pipeMigrations(removeCasesFieldMappings, addHasAuthConfigurationObject)
);

const migrationActionsFourteen = encryptedSavedObjects.createMigration<RawAction, RawAction>(
(doc): doc is SavedObjectUnsanitizedDoc<RawAction> => true,
const migrationActionsFourteen = createMigrationFns(
encryptedSavedObjects,
(doc): doc is SavedObjectUnsanitizedDoc<RawConnector> => true,
pipeMigrations(addisMissingSecretsField)
);

Expand All @@ -54,29 +69,29 @@ export function getMigrations(
}

function executeMigrationWithErrorHandling(
migrationFunc: SavedObjectMigrationFn<RawAction, RawAction>,
migrationFunctions: ConnectorMigrationFns<RawConnector, RawConnector>,
version: string
) {
return (doc: SavedObjectUnsanitizedDoc<RawAction>, context: SavedObjectMigrationContext) => {
return (doc: SavedObjectUnsanitizedDoc<RawConnector>, context: SavedObjectMigrationContext) => {
try {
return migrationFunc(doc, context);
return migrationFunctions.esoMigrationFn(doc, context);
} catch (ex) {
context.log.error<ActionsLogMeta>(
`encryptedSavedObject ${version} migration failed for action ${doc.id} with error: ${ex.message}`,
context.log.error<ConnectorLogMeta>(
`encryptedSavedObject ${version} migration failed for connector ${doc.id} with error: ${ex.message}`,
{
migrations: {
actionDocument: doc,
connectorDocument: doc,
},
}
);
}
return doc;
return migrationFunctions.fallbackMigrationFn(doc, context);
};
}

function renameCasesConfigurationObject(
doc: SavedObjectUnsanitizedDoc<RawAction>
): SavedObjectUnsanitizedDoc<RawAction> {
doc: SavedObjectUnsanitizedDoc<RawConnector>
): SavedObjectUnsanitizedDoc<RawConnector> {
if (!doc.attributes.config?.casesConfiguration) {
return doc;
}
Expand All @@ -95,8 +110,8 @@ function renameCasesConfigurationObject(
}

function removeCasesFieldMappings(
doc: SavedObjectUnsanitizedDoc<RawAction>
): SavedObjectUnsanitizedDoc<RawAction> {
doc: SavedObjectUnsanitizedDoc<RawConnector>
): SavedObjectUnsanitizedDoc<RawConnector> {
if (
!doc.attributes.config?.hasOwnProperty('isCaseOwned') &&
!doc.attributes.config?.hasOwnProperty('incidentConfiguration')
Expand All @@ -115,8 +130,8 @@ function removeCasesFieldMappings(
}

const addHasAuthConfigurationObject = (
doc: SavedObjectUnsanitizedDoc<RawAction>
): SavedObjectUnsanitizedDoc<RawAction> => {
doc: SavedObjectUnsanitizedDoc<RawConnector>
): SavedObjectUnsanitizedDoc<RawConnector> => {
if (doc.attributes.actionTypeId !== '.email' && doc.attributes.actionTypeId !== '.webhook') {
return doc;
}
Expand All @@ -134,8 +149,8 @@ const addHasAuthConfigurationObject = (
};

const addisMissingSecretsField = (
doc: SavedObjectUnsanitizedDoc<RawAction>
): SavedObjectUnsanitizedDoc<RawAction> => {
doc: SavedObjectUnsanitizedDoc<RawConnector>
): SavedObjectUnsanitizedDoc<RawConnector> => {
return {
...doc,
attributes: {
Expand All @@ -145,7 +160,27 @@ const addisMissingSecretsField = (
};
};

function pipeMigrations(...migrations: ActionMigration[]): ActionMigration {
return (doc: SavedObjectUnsanitizedDoc<RawAction>) =>
function pipeMigrations(...migrations: ConnectorMigration[]): ConnectorMigration {
return (doc: SavedObjectUnsanitizedDoc<RawConnector>) =>
migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc);
}

function createMigrationFns(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup,
isMigrationNeededPredicate: IsMigrationNeededPredicate<RawConnector>,
migrationFunc: ConnectorMigration
): ConnectorMigrationFns<RawConnector, RawConnector> {
return {
esoMigrationFn: encryptedSavedObjects.createMigration<RawConnector, RawConnector>(
isMigrationNeededPredicate,
migrationFunc
),
fallbackMigrationFn: (doc: SavedObjectUnsanitizedDoc<RawConnector>) => {
if (!isMigrationNeededPredicate(doc)) {
return doc;
}

return migrationFunc(doc);
},
};
}
Loading