-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
Delete legacy URL aliases when objects are deleted or unshared #117056
Delete legacy URL aliases when objects are deleted or unshared #117056
Conversation
Also needed to update the legacy-url-alias type mapping to make the query work.
bd79e83
to
1be71e8
Compare
1be71e8
to
4236bb9
Compare
Pinging @elastic/kibana-security (Team:Security) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. A few minor comments
// Legacy URL aliases cannot exist in the default space; filter that out | ||
const filteredNamespaces = namespaces.filter( | ||
(namespace) => namespace !== DEFAULT_NAMESPACE_STRING | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't even aware of that tbh...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, aliases are only created when objects in non-default spaces are converted, and I also designed resolve
with that assumption in mind. Technically we could have chosen to support aliases in the default space (with a default:
prefix in the raw ID) but there wasn't any practical reason to do so.
deleteLegacyUrlAliases({ | ||
mappings, | ||
registry, | ||
client, | ||
getIndexForType, | ||
type, | ||
id, | ||
namespaces, | ||
deleteBehavior, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a shame we can't bulkDeleteLegacyUrlAliases
, but given that namespaces
and deleteBehavior
can be different per-object, I'm not even sure this would be doable with a single query/script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I thought about it for a while and I just don't see a way to do this with a single query, unless we passed in tons of painless script parameters (a map that has different values for each object type:id), and I'm not sure how that would perform.
Since deleting and unsharing objects should happen infrequently, I think it's OK to make individual calls to deleteLegacyUrlAliases
, and a simple query and script is much safer and more reliable than a big complex one.
src/core/server/saved_objects/service/lib/update_objects_spaces.test.ts
Outdated
Show resolved
Hide resolved
@@ -53,8 +53,8 @@ export function deleteTestSuiteFactory(es: Client, esArchiver: any, supertest: S | |||
// @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`. | |||
const buckets = response.aggregations?.count.buckets; | |||
|
|||
// The test fixture contains three legacy URL aliases: | |||
// (1) one for "space_1", (2) one for "space_2", and (3) one for "other_space", which is a non-existent space. | |||
// The test fixture contains six legacy URL aliases: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
if (expectAliasDifference !== undefined) { | ||
// if we deleted an object that had an alias pointing to it, the alias should have been deleted as well | ||
await es.indices.refresh({ index: '.kibana' }); // alias deletion uses refresh: false, so we need to manually refresh the index before searching |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIT/optional: I think we could refresh only once if objects.find((obj) => obj.expectAliasDifference !== undefined)
src/core/server/saved_objects/service/lib/legacy_url_aliases/delete_legacy_url_aliases.ts
Outdated
Show resolved
Hide resolved
{ ignore: [404] } | ||
); | ||
} catch (err) { | ||
const { error } = err.body; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're handling expected ES errors here, but an unexpected error may not have a body
property, or that body
property may not have an error
property. If we encounter an error during this error handling, then we'll lose the origial error context and have a difficult time tracking down exactly what happened
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call, I found this function that the core ES client module provides:
kibana/src/core/server/elasticsearch/client/configure_client.ts
Lines 83 to 93 in 2cd007e
/** | |
* Returns a debug message from an Elasticsearch error in the following format: | |
* [error type] error reason | |
*/ | |
export function getErrorMessage(error: errors.ElasticsearchClientError): string { | |
if (error instanceof errors.ResponseError) { | |
const errorBody = error.meta.body as ElasticsearchErrorDetails; | |
return `[${errorBody?.error?.type}]: ${errorBody?.error?.reason ?? error.message}`; | |
} | |
return `[${error.name}]: ${error.message}`; | |
} |
I'll reuse it here
}).catch((err) => { | ||
// The object has already been unshared, but we caught an error when attempting to delete aliases. | ||
// A consumer cannot attempt to unshare the object again, so just log the error and swallow it. | ||
logger.error(err.message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this error have more context?
logger.error(err.message); | |
logger.error(`Failed to delete legacy URL aliases: ${err.message}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error originates from deleteLegacyUrlAliases
, and those error messages are pretty detailed.
Are you suggesting we should change this message to mention that this is happening during updateObjectsSpaces
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error originates from
deleteLegacyUrlAliases
, and those error messages are pretty detailed. Are you suggesting we should change this message to mention that this is happening duringupdateObjectsSpaces
?
I agree that the expected error messages are pretty detailed, and there is currently little room for unexpected errors to occur the way that deleteLegacyUrlAliases
is constructed. Yeah I think mentioning that this is happening during updateObjectsSpaces
would be sufficient. It gives us another piece of "grepable" text in the error message should we ever have to trace this in production
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, done for both consumers in ed54e64
...test/saved_object_api_integration/common/fixtures/es_archiver/saved_objects/spaces/data.json
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM on green CI!
💚 Build Succeeded
Metrics [docs]Saved Objects .kibana field count
History
To update your PR or re-run it, just comment with: |
💚 Backport successful
This backport PR will be merged automatically after passing CI. |
…) (#117461) Co-authored-by: Joe Portner <[email protected]>
Resolves #116235.
Overview
Whenever a Saved Objects Client operation causes an existing legacy URL alias to no longer having a valid target, the alias should be deleted.
There are three situations where a saved object can be removed from a space in Kibana:
This PR aims to address (2) and (3) above.
Implementation
I introduced a new
deleteLegacyUrlAliases
module in Core. This has to be able to delete an alias given an object type, ID, and the space(s) that the object has been removed from. However, if an object is shared, it may have previously existed in numerous spaces and/or "*" (all current and future spaces).Therefore the alias deletion cannot be accomplished with a simple delete or even a bulk operation, we need to query for any "inbound aliases" for a given object in a given set of spaces. The implementation uses the Elasticsearch
updateByQuery
API under the hood, which supports three operations:update
,delete
, andnoop
. We only need to usedelete
andnoop
.There are two consumers of this new module:
SavedObjectsRepository.delete
updateObjectsSpaces
This module is optimized in a few ways:
updateObjectsSpaces
module, we may be potentially updating spaces for dozens or hundreds of different saved objects at the same time, but we potentially have to calldeleteLegacyUrlAliases
for each one; as such, we usep-map
to limit the concurrency ofdeleteLegacyUrlAliases
calls to 10 at a time (we've done this before in theimport
API)Testing
Functional tests have been added to test both "inclusive" and "exclusive" alias deletion in both consumers listed above.
Manual testing for this case is not too straightforward, but you could accomplish it by 1. generating staging data for objects and aliases, 2. adding it to Kibana, 3. calling the HTTP APIs for
delete
orupdateObjectsSpaces
for some of the objects, then 4. use Dev Tools to check and confirm the aliases were deleted as expected.