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

Support generating legacy URL aliases for objects that change IDs during import. #149021

Merged
merged 24 commits into from
Apr 3, 2023

Conversation

azasypkin
Copy link
Member

@azasypkin azasypkin commented Jan 17, 2023

Summary

This PR introduces a new Saved Objects Import/Copy API compatibilityMode boolean parameter that instructs import API to generate legacy URL aliases for all Saved Objects that have to change ID during import/copy.

The aforementioned parameter isn't set by default to avoid any breaking changes.

When importing/copying in the compatibility mode, legacy URL aliases are generated with the purpose field set to savedObjectImport to suppress "We redirected you to a new URL" toast notification. This notification doesn't make sense in this case since the API consumer explicitly opted-in to the compatibility mode.

How to test

HINT: To test compatibilityMode you can either use APIs for import/copy, or you can 1) import/copy in the UI with the enabled Network Dev Tools panel, 2) delete imported/copied content, 3) edit import/copy request in the Dev Tools to include compatibilityMode parameter (or just Copy as cURL if your browser doesn't support request editing) and 4) re-send request.

Import

  1. Create a data view (Data View A) and two dashboards in the default space (Dashboard A and Dashboard B). One of the dashboards (Dashboard B) should include a Markdown visualization with a link (only the part of the URL that starts with #) to another dashboard (Dashboard A), so called weak link.
  2. Export created data view, visualizations and dashboards.
  3. Create a new space (Space A) and import to it the data exported from the default space (via API with compatibilityMode set to true).
  4. Once the import is done, make sure that the weak link works as expected: the link in the Dashboard B in the Space A leads to the Dashboard A in the Space A
  5. Repeat steps 1-4 in the reverse order: create a data view (Data View B) and two dashboards in the Space A (Dashboard C and Dashboard D) etc.

Copy

  1. Create a data view (Data View A) and two dashboards in the default space (Dashboard A and Dashboard B). One of the dashboards (Dashboard B) should include a Markdown visualization with a link (only the part of the URL that starts with #) to another dashboard (Dashboard A), so called weak link.
  2. Create a new space (Space A) and copy created data view, dashboards and visualizations from the default space to this new space (via API with compatibilityMode set to true).
  3. Once the copy is done, make sure that the weak link works as expected: the link in the Dashboard B in the Space A leads to the Dashboard A in the Space A
  4. Repeat steps 1-3 in the reverse order: create a data view (Data View B) and two dashboards in the Space A (Dashboard C and Dashboard D) etc.

Fixes: #123550

@azasypkin azasypkin added skip-ci ci:no-auto-commit Disable auto-committing changes on CI labels Jan 17, 2023
@azasypkin azasypkin force-pushed the issue-123550-weak-links branch 3 times, most recently from 97e1f5b to 1271d15 Compare February 9, 2023 12:52
@azasypkin azasypkin force-pushed the issue-123550-weak-links branch from 1271d15 to 1124f42 Compare February 13, 2023 18:41
/**
* {@inheritDoc ISavedObjectsRepository.getCurrentNamespace}
*/
getCurrentNamespace(namespace?: string) {
Copy link
Member Author

Choose a reason for hiding this comment

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

note: I had to expose this method from the repository and the client to get the current namespace in the import code. The current namespace is required to properly generate legacy URL alias ID.

Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that getCurrentNamespace accepts an optional namespace parameter always bothered me (to a point where I always forget why it is so).

Do we really need to mirror the API definition of SpaceExtension.getCurrentNamespace? Or would it be acceptable to just have a SOR.getCurrentNamespace(-no-parameters-)?

Copy link
Member Author

@azasypkin azasypkin Feb 15, 2023

Choose a reason for hiding this comment

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

Do we really need to mirror the API definition of SpaceExtension.getCurrentNamespace? Or would it be acceptable to just have a SOR.getCurrentNamespace(-no-parameters-)?

That's what I would like to explore further, I agree this namespace argument is super confusing. If I cannot find any use case for that parameter for this newly exposed API, I'll happily get rid of it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fully agreed on the universally despised namespace parameter (kind of wish I had changed this during the refactor). Would there ever be a scenario where spaces as a feature is enabled but the spaces SO extension is not?
As the client doesn't include the parameter, and it is the only external consumer of the new repo method. I say we just get rid of the parameter. I'd be happy to replace the internal repo calls and extension, but I don't want to hold up this PR either.

Copy link
Member Author

Choose a reason for hiding this comment

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

Would there ever be a scenario where spaces as a feature is enabled but the spaces SO extension is not?

I don't think it can be the case (otherwise having or not having the weird namespace parameter in getCurrentNamespace method would be the least of our problems 🙂 )

As the client doesn't include the parameter, and it is the only external consumer of the new repo method.

I removed it from the client's public method signature, but kept in the repository's signature since it's still used internally.

Comment on lines 52 to 56
/**
* If true, Kibana will apply various adjustments to the data that's being imported to maintain compatibility between
* different Kibana versions (e.g. generate legacy URL aliases for all imported objects that have to change IDs).
*/
compatibilityMode: boolean;
Copy link
Member Author

Choose a reason for hiding this comment

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

note: I went back and forth trying to find the appropriate name for this parameter without making a notion of the legacy URL alias a part of the public contract, and ended up with something that's close to what we really do (maintain not-officially-supported "weak-links" behavior) and vague enough to not expose implementation detail. I'd be happy to accept any other better name suggestion though.

// origin ID is maintained.
const requiresLegacyUrlAlias =
compatibilityMode &&
originId !== importStateValue.destinationId &&
Copy link
Member Author

Choose a reason for hiding this comment

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

note: The importStateValue.destinationId === originId is true when, for example, we copy object from default space to a custom space, and then copy that object from a custom space back to a default space

Comment on lines 35 to 42
const originalSavedObject = isLegacyUrlAlias(savedObject)
? originalSavedObjectsMap.get(
`${savedObject.attributes.targetType}:${savedObject.attributes.sourceId}`
) ??
originalSavedObjectsMap.get(
`${savedObject.attributes.targetType}:${savedObject.attributes.targetId}`
)
: originalSavedObjectsMap.get(`${savedObject.type}:${savedObject.id}`);
Copy link
Member Author

Choose a reason for hiding this comment

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

note: I'm not yet sure if we should expose legacy URL alias related errors in the API response, maybe we just need to log them.... I cannot find a non-edge use case when we'd get errors for the legacy URL aliases assuming all "preflight" checks we do in the import code. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

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

If the errors related to legacy url aliases are only edge-case errors (e.g connectivity issue with ES, concurrency conflicts and so on) and non-actionable, it's probably fine to just surface them in the API so that there's a trace somewhere.

Comment on lines +58 to +60
if (object.createNewCopies && object.compatibilityMode) {
return 'cannot use [createNewCopies] with [compatibilityMode]';
}
Copy link
Member Author

Choose a reason for hiding this comment

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

note: If the API consumer explicitly requires new IDs then compatibility mode doesn't make sense.

@azasypkin azasypkin requested a review from a team February 13, 2023 19:03
@azasypkin
Copy link
Member Author

Hey @pgayvallet,

Here's the draft version for the broken-weak-links fix that we've discussed recently. Would you mind sharing your preliminary feedback on the approach and assumptions I made before I proceed with tests, docs etc.?

Thanks!

@azasypkin azasypkin added Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! labels Feb 13, 2023
@azasypkin azasypkin requested review from pgayvallet and removed request for a team February 13, 2023 19:05
@pgayvallet
Copy link
Contributor

Ack: will TAL today

Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

Some comments and remarks, but the approach looks good to me.

/**
* {@inheritDoc ISavedObjectsRepository.getCurrentNamespace}
*/
getCurrentNamespace(namespace?: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that getCurrentNamespace accepts an optional namespace parameter always bothered me (to a point where I always forget why it is so).

Do we really need to mirror the API definition of SpaceExtension.getCurrentNamespace? Or would it be acceptable to just have a SOR.getCurrentNamespace(-no-parameters-)?

// is specified explicitly we should use it instead of the namespace the saved objects client is scoped to. In certain
// scenarios (e.g. copying to a default space) both current namespace and namespace from the parameter aren't defined.
const legacyUrlTargetNamespace = SavedObjectsUtils.namespaceIdToString(
namespace ?? savedObjectsClient.getCurrentNamespace(namespace)
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: namespace ?? savedObjectsClient.getCurrentNamespace()

// filter out the 'version' field of each object, if it exists, and set the originId appropriately
const legacyAliases = new Map<string, SavedObjectsBulkCreateObject>();
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: comment should probably remain at the same position (before the next line)

Comment on lines 35 to 42
const originalSavedObject = isLegacyUrlAlias(savedObject)
? originalSavedObjectsMap.get(
`${savedObject.attributes.targetType}:${savedObject.attributes.sourceId}`
) ??
originalSavedObjectsMap.get(
`${savedObject.attributes.targetType}:${savedObject.attributes.targetId}`
)
: originalSavedObjectsMap.get(`${savedObject.type}:${savedObject.id}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

If the errors related to legacy url aliases are only edge-case errors (e.g connectivity issue with ES, concurrency conflicts and so on) and non-actionable, it's probably fine to just surface them in the API so that there's a trace somewhere.

Comment on lines 98 to 102
const requiresLegacyUrlAlias =
compatibilityMode &&
originId !== importStateValue.destinationId &&
!importStateValue.omitOriginId &&
!legacyAliases.has(originId);
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a big if.

Copy link
Member Author

Choose a reason for hiding this comment

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

Indeed, let me see if I can split it into separate variables just for the readability sake.

Comment on lines 144 to 148
const remappedResults = expectedResults.flatMap<CreatedObject<T>>((result) => {
// Only keep `error` results for the legacy URL aliases to report errors.
if (result.type === LEGACY_URL_ALIAS_TYPE) {
return result.error ? [result] : [];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: took me some time understanding what we're doing with the legacy url aliases in that loop. I would have separated normal results and legacy url alias from expectedResults before the flatMap, and then passed the legacy url alias to extractErrors more explicitly, like with an additional parameter.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's my attempt of the premature performance optimization, tried to avoid looping through the list twice, but based on the previous comment we might split these calls so it won't be a problem anymore.

then passed the legacy url alias to extractErrors more explicitly, like with an additional parameter.

++, sounds good.

Comment on lines 135 to 138
const bulkCreateResponse = await savedObjectsClient.bulkCreate(
[...objectsToCreate, ...Array.from(legacyAliases.values())],
{ namespace, overwrite, refresh }
);
Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed sync: Ideally, to avoid impact on edge cases, we would create the objects first, and then the legacy url aliases. But that's fine.

Copy link
Member Author

Choose a reason for hiding this comment

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

My bad, for some reason I though we agreed on the opposite 🙂 Anyway, I'll separate these calls just to be on the safe side.

Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding was that we agreed that having a single bulkCreate was totally acceptable as it was easier, compared to do 2 distinct calls 😅

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, right, I've just "heard" what I wanted to hear 🙂

@azasypkin
Copy link
Member Author

Some comments and remarks, but the approach looks good to me.

Thanks for the review! I'll handle your feedback and proceed with the tests, and tag you/core for review once it's ready.

@azasypkin azasypkin removed the skip-ci label Mar 10, 2023
Copy link
Contributor

@pgayvallet pgayvallet left a comment

Choose a reason for hiding this comment

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

Looking good to me

Comment on lines 416 to 418
/**
* Returns namespace associated with the client, if any.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: (and because I'm not even sure myself that early in the morning) we may want to explain/document what the expected return is when we are in the default namespace (default or undefined?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, good point, I'll mention this in the comment - for default namespace (or when space extension isn't enabled) this method returns undefined

Comment on lines +138 to +139
const objectRequiresLegacyUrlAlias = !!result.originId && result.originId !== result.id;
if (compatibilityMode && objectRequiresLegacyUrlAlias && objectSuccessfullyImported) {
Copy link
Contributor

Choose a reason for hiding this comment

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

That's a way better if compared to first version 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

I did my best! 😆

CORE_USAGE_STATS_TYPE,
CORE_USAGE_STATS_ID,
[
`${RESOLVE_IMPORT_STATS_PREFIX}.total`,
`${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.total`,
`${RESOLVE_IMPORT_STATS_PREFIX}.namespace.default.kibanaRequest.yes`,
`${RESOLVE_IMPORT_STATS_PREFIX}.createNewCopiesEnabled.yes`,
// 'compatibilityModeEnabled.yes` and `compatibilityModeEnabled.no` when createNewCopies is true
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: incorrect or partially copied comment?

@azasypkin azasypkin added release_note:skip Skip the PR/issue when compiling release notes and removed release_note:enhancement labels Mar 29, 2023
@jeramysoucy jeramysoucy self-requested a review March 29, 2023 12:11
Copy link
Contributor

@jeramysoucy jeramysoucy left a comment

Choose a reason for hiding this comment

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

Looks great! I just had some questions and a minor nit.

NOTE: This cannot be used with the `createNewCopies` option.
NOTE: This option cannot be used with the `createNewCopies` option.

`compatibilityMode`::
Copy link
Contributor

Choose a reason for hiding this comment

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

Question (just for my own sanity) - is there a reason why some API's expect options as query parameters and others as fields within the request body?

Copy link
Member Author

@azasypkin azasypkin Mar 31, 2023

Choose a reason for hiding this comment

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

That's a good question, and I'm not quite sure. I don't think there is any good reason why import and copy APIs accept similar options differently.

The import API uses query string parameters because the body is the raw file content, so it's understandable why it uses both body and query string parameters. As for the copy API, having spaces and objects parameters in the body makes sense since they might be quite large potentially, but I'd rather use query string for the rest of the parameters to be consistent with the import API. I guess, we put everything in the body just to make it simpler for us 🤷‍♂️

/**
* {@inheritDoc ISavedObjectsRepository.getCurrentNamespace}
*/
getCurrentNamespace(namespace?: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Fully agreed on the universally despised namespace parameter (kind of wish I had changed this during the refactor). Would there ever be a scenario where spaces as a feature is enabled but the spaces SO extension is not?
As the client doesn't include the parameter, and it is the only external consumer of the new repo method. I say we just get rid of the parameter. I'd be happy to replace the internal repo calls and extension, but I don't want to hold up this PR either.

});
});

describe('with a defined namespace', () => {
const namespace = 'some-namespace';
test('calls bulkCreate once with input objects', async () => {
await testBulkCreateObjects(namespace);
await testBulkCreateObjects({ namespace });
await testBulkCreateObjects({ namespace, compatibilityMode: true });
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a nit: update test description 'calls bulkCreate according to input objects and compatibilityMode option', as with compatibilityMode it is called twice. Or separate the compatibilityMode tests into a subset for more specific description wording.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, will update method description!

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
@kbn/core-saved-objects-api-server 4 5 +1
@kbn/core-saved-objects-api-server-internal 54 55 +1
total +2
Unknown metric groups

API count

id before after diff
@kbn/core-saved-objects-api-server 338 341 +3
@kbn/core-saved-objects-api-server-internal 75 77 +2
@kbn/core-saved-objects-server 491 493 +2
total +7

ESLint disabled line counts

id before after diff
securitySolution 432 435 +3

Total ESLint disabled count

id before after diff
securitySolution 512 515 +3

History

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

@azasypkin azasypkin merged commit a1fccfd into elastic:main Apr 3, 2023
@azasypkin azasypkin deleted the issue-123550-weak-links branch April 3, 2023 08:54
@kibanamachine kibanamachine added v8.8.0 backport:skip This commit does not require backporting labels Apr 3, 2023
* If true, Kibana will apply various adjustments to the data that's being retried to import to maintain compatibility between
* different Kibana versions (e.g. generate legacy URL aliases for all imported objects that have to change IDs).
*/
compatibilityMode?: boolean;
Copy link
Contributor

@TinaHeiligers TinaHeiligers Apr 21, 2023

Choose a reason for hiding this comment

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

@azasypkin I'm working in code related to import saved objects and went hunting down how compatibilityMode works in resolve_import_errors. The docs for this HTTP API need to be updated to include the new query option.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, good catch, thanks @TinaHeiligers! I'll update docs for this API as well.

drewdaemon added a commit that referenced this pull request Jan 16, 2024
## Summary

The broken weak links problem wasn't fixed in 8.1, so this statement is
now confusing.

An aliasing workaround [was
added](#149021) in 8.8 for those
customers who need it.

Better solutions
- use the [new links
panel](https://www.elastic.co/guide/en/kibana/current/dashboard-links.html)
for navigation between dashboards
- use [shareable
dashboards](#167901) to make
analytics stuff available in multiple spaces instead of copying (not yet
available)
CoenWarmer pushed a commit to CoenWarmer/kibana that referenced this pull request Feb 15, 2024
## Summary

The broken weak links problem wasn't fixed in 8.1, so this statement is
now confusing.

An aliasing workaround [was
added](elastic#149021) in 8.8 for those
customers who need it.

Better solutions
- use the [new links
panel](https://www.elastic.co/guide/en/kibana/current/dashboard-links.html)
for navigation between dashboards
- use [shareable
dashboards](elastic#167901) to make
analytics stuff available in multiple spaces instead of copying (not yet
available)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting ci:no-auto-commit Disable auto-committing changes on CI release_note:skip Skip the PR/issue when compiling release notes Team:Core Core services & architecture: plugins, logging, config, saved objects, http, ES client, i18n, etc Team:Security Team focused on: Auth, Users, Roles, Spaces, Audit Logging, and more! v8.8.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Importing saved objects can break "weak links"
7 participants