-
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
Support generating legacy URL aliases for objects that change IDs during import. #149021
Conversation
97e1f5b
to
1271d15
Compare
1271d15
to
1124f42
Compare
/** | ||
* {@inheritDoc ISavedObjectsRepository.getCurrentNamespace} | ||
*/ | ||
getCurrentNamespace(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.
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.
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 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-)
?
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.
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.
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.
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.
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.
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.
/** | ||
* 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; |
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.
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 && |
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.
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
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}`); |
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.
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?
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 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.
if (object.createNewCopies && object.compatibilityMode) { | ||
return 'cannot use [createNewCopies] with [compatibilityMode]'; | ||
} |
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.
note: If the API consumer explicitly requires new IDs then compatibility mode doesn't make sense.
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! |
Ack: will TAL today |
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.
Some comments and remarks, but the approach looks good to me.
/** | ||
* {@inheritDoc ISavedObjectsRepository.getCurrentNamespace} | ||
*/ | ||
getCurrentNamespace(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.
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) |
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: 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>(); |
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: comment should probably remain at the same position (before the next line)
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}`); |
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 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.
const requiresLegacyUrlAlias = | ||
compatibilityMode && | ||
originId !== importStateValue.destinationId && | ||
!importStateValue.omitOriginId && | ||
!legacyAliases.has(originId); |
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 big if.
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.
Indeed, let me see if I can split it into separate variables just for the readability sake.
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] : []; | ||
} |
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: 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.
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 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.
const bulkCreateResponse = await savedObjectsClient.bulkCreate( | ||
[...objectsToCreate, ...Array.from(legacyAliases.values())], | ||
{ namespace, overwrite, refresh } | ||
); |
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.
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.
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.
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.
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.
My understanding was that we agreed that having a single bulkCreate was totally acceptable as it was easier, compared to do 2 distinct calls 😅
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.
Right, right, I've just "heard" what I wanted to hear 🙂
Thanks for the review! I'll handle your feedback and proceed with the tests, and tag you/core for review once it's ready. |
…sfully imported. Remove `namespace` parameter from the `getCurrentNamespace` SO Client API. Include legacy URL aliases errors in the import API response.
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.
Looking good to me
/** | ||
* Returns namespace associated with the client, if any. | ||
*/ |
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: (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
?)
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, good point, I'll mention this in the comment - for default namespace (or when space extension isn't enabled) this method returns undefined
const objectRequiresLegacyUrlAlias = !!result.originId && result.originId !== result.id; | ||
if (compatibilityMode && objectRequiresLegacyUrlAlias && objectSuccessfullyImported) { |
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 way better if
compared to first version 😄
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 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 |
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: incorrect or partially copied comment?
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.
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`:: |
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.
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?
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 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) { |
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.
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 }); |
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.
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.
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 catch, will update method description!
💚 Build Succeeded
Metrics [docs]Public APIs missing comments
Unknown metric groupsAPI count
ESLint disabled line counts
Total ESLint disabled count
History
To update your PR or re-run it, just comment with: |
* 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; |
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.
@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.
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.
Ah, good catch, thanks @TinaHeiligers! I'll update docs for this API as well.
## 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)
## 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)
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 tosavedObjectImport
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 includecompatibilityMode
parameter (or justCopy as cURL
if your browser doesn't support request editing) and 4) re-send request.Import
Data View A
) and two dashboards in the default space (Dashboard A
andDashboard 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.Space A
) and import to it the data exported from the default space (via API withcompatibilityMode
set totrue
).Dashboard B
in theSpace A
leads to theDashboard A
in theSpace A
Data View B
) and two dashboards in theSpace A
(Dashboard C
andDashboard D
) etc.Copy
Data View A
) and two dashboards in the default space (Dashboard A
andDashboard 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.Space A
) and copy created data view, dashboards and visualizations from the default space to this new space (via API withcompatibilityMode
set totrue
).Dashboard B
in theSpace A
leads to theDashboard A
in theSpace A
Data View B
) and two dashboards in theSpace A
(Dashboard C
andDashboard D
) etc.Fixes: #123550