[Ingest Manager] Adding bulk packages upgrade api#77827
[Ingest Manager] Adding bulk packages upgrade api#77827jonathan-buttner merged 13 commits intoelastic:masterfrom
Conversation
|
Pinging @elastic/ingest-management (Feature:EPM) |
|
Pinging @elastic/ingest-management (Team:Ingest Management) |
| export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; | ||
|
|
||
| // EPM API routes | ||
| const EPM_PACKAGES_BULK = `${EPM_API_ROOT}/packages_bulk`; |
There was a problem hiding this comment.
I was hoping to use POST /packages but that is used by the local package upload install API.
There was a problem hiding this comment.
packages/_bulk like ES also occurred to me but I haven't looked into it.
There was a problem hiding this comment.
I just put up a bulk actions PR #77690 where I added /bulk_reassign and /bulk_unenroll routes. does it make sense to agree on a convention here?
There was a problem hiding this comment.
I was gonna ask the same thing. Seeing yours makes me say keep bulk_*.
There was a problem hiding this comment.
Thanks for the feedback so are we thinking /bulk_packages then? Or should I go with /packages/_bulk if that works?
There was a problem hiding this comment.
imo this points to the need to add a verb to this route. is my understanding correct that this route is only for upgrading existing packages, rather than being able to bulk install packages? if so, I think it makes sense to differentiate this from the install routes (/packages). what do you all think of using /packages/bulk_upgrade?:
BULK_UPGRADE_PATTERN: `${EPM_PACKAGES_MANY}/bulk_upgrade`
There was a problem hiding this comment.
@jen-huang It can do both upgrades and installs. If the package hasn't already been installed, it'll just install the latest one available.
There was a problem hiding this comment.
in that case I'm happy to leave the route as /packages_bulk, or simply switching to bulk_packages to follow more closely with the bulk actions API convention. although /_bulk is an ES convention, I've never felt that Kibana APIs need to follow ES patterns
There was a problem hiding this comment.
@jen-huang @neptunian @jfsiii I'm happy to switch it to bulk_packages unless someone objects?
There was a problem hiding this comment.
I'm not too fussed but I don't think bulk_packages really follows the convention because those are using bulk_(verb) like bulk_upgrade. Since we don't have the verb, it seems redundant to say packages/bulk_packages.
| error, | ||
| response, | ||
| }: IngestErrorHandlerParams): Promise<IKibanaResponse> => { | ||
| export function handleIngestError(error: IngestManagerError | Boom | Error) { |
There was a problem hiding this comment.
Extracted this out to its own function so the upgrade api can use it to create an array of errors if an error occurs while upgrading a package.
There was a problem hiding this comment.
I like the separate function. The only reason it doesn't exist yet is I couldn't come up with a name for the interface it returns.
We can keep thinking and update later.
There was a problem hiding this comment.
To clarify, I'm OK with this as submitted. We can change now or later. Some things I was thinking about:
RequestHandler's and IngestErrorHandlers both end in a IKibanaResponse
This function (or this type of function) maps an error to a status code (number) or an interface that contains a status code an maybe some other info.
I've yet to figure out what to call it, and was leaning towards a shape like you have here but delayed worrying about it until it came up again.
There was a problem hiding this comment.
Oh, maybe we could use CustomHttpResponseOptions or Pick<CustomHttpResponseOptions, 'body' | 'statusCode'>
kibana/src/core/server/http/router/response.ts
Lines 83 to 93 in feceb0f
There was a problem hiding this comment.
Ah ok, so just to make sure I'm understanding correctly, changing the function signature to this:
export function handleIngestError(error: IngestManagerError | Boom | Error): Pick<CustomHttpResponseOptions, 'body' | 'statusCode'> {
Is what you're suggesting?
| // could have also done `return defaultIngestErrorHandler({ error: e, response })` at each of the returns, | ||
| // but doing it this way will log the outer/install errors before any inner/rollback errors | ||
| const defaultResult = await defaultIngestErrorHandler({ error: e, response }); | ||
| if (e instanceof IngestManagerError) { |
There was a problem hiding this comment.
Extracted this into a function that is shared between the installPackageFromRegistryHandler handler and the bulk upgrade handler.
x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts
Outdated
Show resolved
Hide resolved
| } | ||
|
|
||
| // TODO I think we want version here and not `install_version`? | ||
| if (!installedPkg || semver.gt(latestPackage.version, installedPkg.attributes.version)) { |
There was a problem hiding this comment.
Thoughts on wether this is the right version I'm using here?
There was a problem hiding this comment.
yes, version is the currently installed version
| oldVersion: latestPackage.version, | ||
| assets: [ | ||
| ...installedPkg.attributes.installed_es, | ||
| ...installedPkg.attributes.installed_kibana, |
There was a problem hiding this comment.
In the case where no upgrade actually occurs because it was already at the latest version, should we return the current assets? I think this is all the installed assets?
There was a problem hiding this comment.
Good question. Currently we reinstall the package if its already installed, but this functionality would not, so it would be a bit inconsistent, though I'm not sure we should be reinstalling anyway. I think for now its fine to just return the assets.
jfsiii
left a comment
There was a problem hiding this comment.
Thanks so much for this PR. I left some comments and would like to make some changes, but the important bits look good and the tests will make it easier.
I'll follow up tomorrow
| } | ||
|
|
||
| export async function handleInstallPackageFailure( | ||
| savedObjectsClient: SavedObjectsClientContract, |
There was a problem hiding this comment.
Please convert this to an object. 6 parameters is about 2x my max.
| export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency'; | ||
|
|
||
| // EPM API routes | ||
| const EPM_PACKAGES_BULK = `${EPM_API_ROOT}/packages_bulk`; |
There was a problem hiding this comment.
packages/_bulk like ES also occurred to me but I haven't looked into it.
| error, | ||
| response, | ||
| }: IngestErrorHandlerParams): Promise<IKibanaResponse> => { | ||
| export function handleIngestError(error: IngestManagerError | Boom | Error) { |
There was a problem hiding this comment.
I like the separate function. The only reason it doesn't exist yet is I couldn't come up with a name for the interface it returns.
We can keep thinking and update later.
| response: AssetReference[]; | ||
| } | ||
|
|
||
| export interface UpgradePackageError { |
There was a problem hiding this comment.
This name reads like it's an actual Error. Mind prefixing with an I for IUpgradePackageError or adding something to the end?
| }): Promise<Array<UpgradePackageInfo | UpgradePackageError>> { | ||
| const res: Array<UpgradePackageInfo | UpgradePackageError> = []; | ||
|
|
||
| for (const pkgToUpgrade of packagesToUpgrade) { |
There was a problem hiding this comment.
Since we don't care want to stop on the first failure, I think we can use Promise.allSettled https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled and loop the results and use the status property to test success/failure
There was a problem hiding this comment.
Ah thanks!
There was a problem hiding this comment.
John and I talked about this and I think we're going to leave it as is for now because the code I came up with using allSettled wasn't super readable.
There was a problem hiding this comment.
After we spoke, I took a pass at it #77974
As I said on the call, I'm happy to ship existing version but that PR is essentially what I was describing
There was a problem hiding this comment.
Thanks John! I'll pull in your changes.
| version: latestPackage.version, | ||
| }); | ||
|
|
||
| try { |
There was a problem hiding this comment.
Same comment re: Promise.allSettled
| } | ||
|
|
||
| export interface UpgradePackagesResponse { | ||
| response: Array<UpgradePackageInfo | UpgradePackageError>; |
There was a problem hiding this comment.
We have similar patterns elsewhere. Take a look at https://github.com/elastic/kibana/pull/77690/files#diff-f1874bde49bc5bca72d42584b63a5cf3R143-R147 & https://github.com/elastic/kibana/blob/71b9dedfc4eadd97db2b6b0dc038d0f640babf7b/x-pack/plugins/ingest_manager/common/types/rest_spec/package_policy.ts#L53-L57
I like error: Error if it's possible since it retains important info and can easily be handled/converted by others.
There was a problem hiding this comment.
Do you think it makes more sense to have the response of the bulk endpoint be something like:
export interface UpgradePackageInfo {
name: string;
newVersion?: string;
// this will be null if no package was present before the upgrade (aka it was an install)
oldVersion?: string | null;
assets?: AssetReference[];
statusCode: number;
error?: Error;
}
If there's an error then, newVersion, oldVersion, assets will be undefined.
export interface UpgradePackagesResponse {
response: UpgradePackageInfo[];
}
|
It looks like this change will install the latest package if it doesn't exist. Which makes it similar to our current route |
Yeah good point, I can switch everything over to "install" instead of upgrade. |
|
There was an issue to change from POST to PUT to make the behavior of the route more clear but not sure we'll get around to that or how necessary it is: #62479 |
| expect(entry.oldVersion).equal('0.1.0'); | ||
| expect(entry.newVersion).equal('0.3.0'); | ||
| }); | ||
| it('should the same package multiple times for upgrade', async function () { |
There was a problem hiding this comment.
description missing a word I think
There was a problem hiding this comment.
Thanks I'll update.
| const { body }: { body: BulkInstallPackagesResponse } = await supertest | ||
| .post(`/api/ingest_manager/epm/packages/_bulk`) | ||
| .set('kbn-xsrf', 'xxxx') | ||
| .send({ packages: ['multiple_versions', 'multiple_versions'] }) |
There was a problem hiding this comment.
Is there a user story/case for this? The test confirms it's a no-op. Why would a user submit the same name multiple times?
There was a problem hiding this comment.
No, there's no user story. There's no real reason we need to support it. I was just trying to guard against the case where it happened accidentally. I can remove the test if you think it's confusing.
There was a problem hiding this comment.
I removed the test.
| }): Promise<Array<UpgradePackageInfo | UpgradePackageError>> { | ||
| const res: Array<UpgradePackageInfo | UpgradePackageError> = []; | ||
|
|
||
| for (const pkgToUpgrade of packagesToUpgrade) { |
There was a problem hiding this comment.
After we spoke, I took a pass at it #77974
As I said on the call, I'm happy to ship existing version but that PR is essentially what I was describing
💚 Build SucceededBuild metricspage load bundle size
History
To update your PR or re-run it, just comment with: |
* Adding bulk upgrade api * Addressing comments * Removing todo * Changing body field * Adding helper for getting the bulk install route * Adding request spec * Pulling in Johns changes * Removing test for same package upgraded multiple times * Pulling in John's error handling changes * Fixing type error
* Adding bulk upgrade api * Addressing comments * Removing todo * Changing body field * Adding helper for getting the bulk install route * Adding request spec * Pulling in Johns changes * Removing test for same package upgraded multiple times * Pulling in John's error handling changes * Fixing type error
This PR adds a new api
POST /packages_bulkto handle bulk upgrading of packages. The api enforces that the latest version must be greater than the current version. It returns an array of entries corresponding to the requested package upgrades. An entry contains the information returned from the regular install function, the package name, and the old and new versions.More info on the design is here: #77779
I refactored a couple functions that are shared between the bulk upgrade and the install package apis.
Once this is merged I'll have the Security Solution use this api to upgrade the
endpointpackage by refactoring the code here: #77498And add similar functionality in the Ingest Manager's UI to upgrade the default packages when a user is navigating between the pages.