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

[Cases] Migrate connector ID to references #104221

Merged

Conversation

jonathan-buttner
Copy link
Contributor

@jonathan-buttner jonathan-buttner commented Jul 1, 2021

Background: #105677

This PR Migrates the actions connector ID fields we have stored in the Configuration and Case saved objects. We still need to finish user actions, which will be done in a subsequent PR.

The general approach I took was to move all transformations into the service layer. There is now an external model which is returned the UI and used in most places within the backend. The internal (ES) model is used only within the service layer.

Going from the external model to the ES model

Prior to this change we were already transform the connector.fields from an object to an array of objects that had a key and value (i.e. for jira {key: 'issueType', value: 'bug'})

I moved this transformation into the service layer as well.

In addition to the fields transformation I remove the connector.id (for the configuration and cases saved objects) and move it to the references array.

Going from the ES model to the external model

Prior to this change we already transformed the connector.fields from the ES representation of an array of objects I mentioned above into an object with the fields like: {issueType: bug} to follow the example above.

This was also moved to the service layer. So after the saved object client does a get, post, update, find, etc we transform the ES model into the external model by converting the connector.fields, finding the id from the references array and placing it back in connector.id and external_service.connector_id.

Saved Object Migration

In addition to moving the transformation logic to the service layer we also need to the existing saved objects using a migration. I split out the configuration and cases migrations into their own files since the migrations.ts file was getting kind of large.

The migration removes the connector.id for the cases and configuration saved objects and moves it to the references array. For external_service.connector_id it does the same thing.

Errors

Going from the ES model to the external model

If the transformation can't find a reference called connectorId inside the references array it will set the connector field to the default none connector (this is what the cases and configuration saved objects should have if no connector is defined.)

If the transformation can't find a reference called pushConnectorId inside the references array it will set the external_service.connector_id field to null. It leaves the rest of the fields in external_service. I did this intentionally so that we wouldn't accidentally get data loss since the external_service saves a good bit of information about the push details.

Going from the external model to the ES model

If the connector.id or external_service.connector_id are value none the transformation will not create a reference for them. it will still remove the connector id field though. If they are defined but not none then it will move the value into the references field.

If the connector.id is undefined for some reason, we won't create a reference.
If the external_service is null or the external_service.connector_id is null it won't create a reference.

Testing

I tried to add good coverage using unit tests for the migration and transformations. I also added a few integration tests that look in the saved object to ensure that the ids are removed appropriately.

@jonathan-buttner jonathan-buttner added Feature:Cases Cases feature release_note:skip Skip the PR/issue when compiling release notes Team:Threat Hunting Security Solution Threat Hunting Team labels Jul 12, 2021
* The external service fields. Exporting here for use in the service transformation code so I can define
* a type without the connector_id field.
*/
export const CaseExternalServiceBasicRt = rt.type({
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exporting here for use in the service transformation code so I can define a type without the connector_id field.

rt.intersection([
CaseExternalServiceBasicRt,
rt.type({
pushed_at: rt.string,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved these up into the basic definition above.

export type ESCasePatchRequest = Omit<CasePatchRequest, 'connector'> & {
connector?: ESCaseConnector;
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These have been moved into the cases service because they are isolated to the service layer. Other parts of cases no longer need to reference the types. Instead they will always reference the CaseAttributes or equivalent type.

export type ESCasesConfigureAttributes = Omit<CasesConfigureAttributes, 'connector'> & {
connector: ESCaseConnector;
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been moved into the configure service because it is isolated to the service layer. Other parts of the cases plugin no longer need to reference the type. Instead they will reference the CasesConfigureAttributes type directly.

name: string;
type: ESCaseConnectorTypes;
fields: ESConnectorFields | null;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were moved into the service layer as well since they are only referenced by service layer functionality.

} catch (error) {
this.log.debug(`Error on UPDATE case configuration ${configurationId}: ${error}`);
throw error;
}
}
}

function transformUpdateResponseToExternalModel(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 : Configuration saved object transformation logic

@@ -0,0 +1,199 @@
/*
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file holds test utility functions.

@@ -0,0 +1,100 @@
/*
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 : Shared transformation logic between the case and configuration saved objects

field: UserActionFieldType
) => {
return field === 'connector' && so.attributes.connector
? transformESConnectorToCaseConnector(so.attributes.connector)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we no longer need to do the transformation, we no longer need the abstraction for getField both can just use get(so, ['attributes', field]) directly as done above.

@@ -605,6 +609,74 @@ export const getConnectorMappingsFromES = async ({ es }: { es: KibanaClient }) =
return mappings;
};

type ESConnectorFields = Array<{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These make the integration test types easier. Since these types are no longer defined in the cases common area we need to define them here so we can access them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is ok to import them from x-pack/plugins/cases/server/services/cases

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I'll give that a shot, I think it was giving me an error, I suppose for this we could just ignore the eslint. Good point.

Comment on lines 94 to 119
const { connector, external_service, ...restAttributes } = caseAttributes;

let transformedAttributes: Partial<ESCaseAttributes> = { ...restAttributes };
let pushConnectorId: string | undefined | null;

if (external_service) {
let restExternalService: ExternalServicesWithoutConnectorId | null | undefined;
({ connector_id: pushConnectorId, ...restExternalService } = external_service);
transformedAttributes = {
...transformedAttributes,
external_service: restExternalService,
};
} else if (external_service === null) {
transformedAttributes = { ...transformedAttributes, external_service: null };
}

if (connector) {
transformedAttributes = {
...transformedAttributes,
connector: {
name: connector.name,
type: connector.type,
fields: transformFieldsToESModel(connector),
},
};
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot think of a way to remove the casting (restExternalService as ExternalServicesWithoutConnectorId ). TS won this fight 😄 .

Suggested change
const { connector, external_service, ...restAttributes } = caseAttributes;
let transformedAttributes: Partial<ESCaseAttributes> = { ...restAttributes };
let pushConnectorId: string | undefined | null;
if (external_service) {
let restExternalService: ExternalServicesWithoutConnectorId | null | undefined;
({ connector_id: pushConnectorId, ...restExternalService } = external_service);
transformedAttributes = {
...transformedAttributes,
external_service: restExternalService,
};
} else if (external_service === null) {
transformedAttributes = { ...transformedAttributes, external_service: null };
}
if (connector) {
transformedAttributes = {
...transformedAttributes,
connector: {
name: connector.name,
type: connector.type,
fields: transformFieldsToESModel(connector),
},
};
}
const { connector, external_service, ...restAttributes } = caseAttributes;
const { connector_id: pushConnectorId, ...restExternalService } = external_service ?? {};
const transformedAttributes = {
...restAttributes,
...(external_service != null
? {
external_service: restExternalService as ExternalServicesWithoutConnectorId,
}
: { external_service: null }),
...(connector != null
? {
connector: {
name: connector.name,
type: connector.type,
fields: transformFieldsToESModel(connector),
},
}
: {}),
};

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, thanks, I took another stab at this let me know what you think.

Comment on lines 134 to 141
if (connectorId === noneConnectorId) {
connectorRef.push({ name: connectorIdReferenceName });
} else if (connectorId) {
connectorRef.push({
name: connectorIdReferenceName,
ref: { id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE },
});
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
if (connectorId === noneConnectorId) {
connectorRef.push({ name: connectorIdReferenceName });
} else if (connectorId) {
connectorRef.push({
name: connectorIdReferenceName,
ref: { id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE },
});
}
connectorRef.push({
name: connectorIdReferenceName,
...(connectorId !== noneConnectorId ? { ref: { id: connectorId, name: connectorIdReferenceName, type: ACTION_SAVED_OBJECT_TYPE }} : {}),
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored this with the introduction of the ConnectorReferenceHandler. Let me know what you think.

* The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference
* field's name property.
*/
export const connectorIdReferenceName = 'connectorId';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: connectorIdReferenceName -> CONNECTOR_ID_REFERENCE_NAME

Maybe we could move these variables to a constants.ts file in x-pack/plugins/cases/server

})
: { id: thisCase.id, version: thisCase.version };
});
const updateCases: UpdateRequestWithOriginalCase[] = query.cases.reduce(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block now zips togethers the original case if it existed and the updated request. This is particularly useful for when we call the patch request so that we can populate the originalCase field. If the original case cannot be found within the map we skip the request. This shouldn't even happen though because the code above should handle finding requests that don't exist.

`);
});

it('builds references for connector_id, connector.id, and includes the existing references', async () => {
Copy link
Member

@cnasikas cnasikas Aug 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: for connector_id, connector.id -> for external service connector id, case connector id or we can put a comment to explain what are those in the whole file test.

} from './transform';

describe('service transform helpers', () => {
describe('findConnectorIdReference', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have two references with the same id?

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Metrics [docs]

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
cases 529.0KB 529.1KB +32.0B

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
cases 131.7KB 131.9KB +231.0B

Saved Objects .kibana field count

Every field in each saved object type adds overhead to Elasticsearch. Kibana needs to keep the total field count below Elasticsearch's default limit of 1000 fields. Only specify field mappings for the fields you wish to search on or query. See https://www.elastic.co/guide/en/kibana/master/development-plugin-saved-objects.html#_mappings

id before after diff
cases 42 40 -2
cases-configure 20 19 -1
total -3
Unknown metric groups

API count

id before after diff
cases 451 443 -8

API count missing comments

id before after diff
cases 409 401 -8

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

cc @jonathan-buttner

Copy link
Member

@cnasikas cnasikas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job as always!

I created a few connectors on master. Then I switched to your PR to test the migration. I also played around with the UI. I could not break anything 🚀. LGTM!

@jonathan-buttner jonathan-buttner merged commit 96f27b9 into elastic:master Aug 4, 2021
@jonathan-buttner jonathan-buttner deleted the migrate-connector-id branch August 4, 2021 14:39
@kibanamachine kibanamachine added the backport missing Added to PRs automatically when the are determined to be missing a backport. label Aug 6, 2021
@kibanamachine
Copy link
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create backports run node scripts/backport --pr 104221 or prevent reminders by adding the backport:skip label.

streamich pushed a commit to vadimkibana/kibana that referenced this pull request Aug 8, 2021
* Starting configure migration

* Initial refactor of configuration connector id

* Additional clean up and tests

* Adding some tests

* Finishing configure tests

* Starting case attributes transformation refactor

* adding more tests for the cases service

* Adding more functionality and tests for cases migration

* Finished unit tests for cases transition

* Finished tests and moved types

* Cleaning up type names

* Fixing types and renaming

* Adding more tests directly for the transformations

* Fixing tests and renaming some functions

* Adding transformation helper tests

* Adding migration utility tests and some clean up

* Begining logic to remove references when it is the none connector

* Fixing merge reference bug

* Addressing feedback

* Changing test name and creating constants file
@kibanamachine
Copy link
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create backports run node scripts/backport --pr 104221 or prevent reminders by adding the backport:skip label.

2 similar comments
@kibanamachine
Copy link
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create backports run node scripts/backport --pr 104221 or prevent reminders by adding the backport:skip label.

@kibanamachine
Copy link
Contributor

Friendly reminder: Looks like this PR hasn’t been backported yet.
To create backports run node scripts/backport --pr 104221 or prevent reminders by adding the backport:skip label.

jonathan-buttner added a commit to jonathan-buttner/kibana that referenced this pull request Aug 11, 2021
* Starting configure migration

* Initial refactor of configuration connector id

* Additional clean up and tests

* Adding some tests

* Finishing configure tests

* Starting case attributes transformation refactor

* adding more tests for the cases service

* Adding more functionality and tests for cases migration

* Finished unit tests for cases transition

* Finished tests and moved types

* Cleaning up type names

* Fixing types and renaming

* Adding more tests directly for the transformations

* Fixing tests and renaming some functions

* Adding transformation helper tests

* Adding migration utility tests and some clean up

* Begining logic to remove references when it is the none connector

* Fixing merge reference bug

* Addressing feedback

* Changing test name and creating constants file
jonathan-buttner added a commit that referenced this pull request Aug 11, 2021
* Starting configure migration

* Initial refactor of configuration connector id

* Additional clean up and tests

* Adding some tests

* Finishing configure tests

* Starting case attributes transformation refactor

* adding more tests for the cases service

* Adding more functionality and tests for cases migration

* Finished unit tests for cases transition

* Finished tests and moved types

* Cleaning up type names

* Fixing types and renaming

* Adding more tests directly for the transformations

* Fixing tests and renaming some functions

* Adding transformation helper tests

* Adding migration utility tests and some clean up

* Begining logic to remove references when it is the none connector

* Fixing merge reference bug

* Addressing feedback

* Changing test name and creating constants file
@kibanamachine kibanamachine removed the backport missing Added to PRs automatically when the are determined to be missing a backport. label Aug 11, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:Cases Cases feature release_note:skip Skip the PR/issue when compiling release notes Team:Threat Hunting Security Solution Threat Hunting Team v7.15.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants