-
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
[Ingest Manager] Adding bulk packages upgrade api #77827
[Ingest Manager] Adding bulk packages upgrade api #77827
Conversation
Pinging @elastic/ingest-management (Feature:EPM) |
Pinging @elastic/ingest-management (Team:Ingest Management) |
@@ -14,10 +14,12 @@ export const FLEET_API_ROOT = `${API_ROOT}/fleet`; | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
packages/_bulk
like ES also occurred to me but I haven't looked into 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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was gonna ask the same thing. Seeing yours makes me say keep bulk_*.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jen-huang @neptunian @jfsiii I'm happy to switch it to bulk_packages
unless someone objects?
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'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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify, I'm OK with this as submitted. We can change now or later. Some things I was thinking about:
RequestHandler's
and IngestErrorHandler
s 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
/** | |
* HTTP response parameters for a response with adjustable status code. | |
* @public | |
*/ | |
export interface CustomHttpResponseOptions<T extends HttpResponsePayload | ResponseError> { | |
/** HTTP message to send to the client */ | |
body?: T; | |
/** HTTP Headers with additional information about response */ | |
headers?: ResponseHeaders; | |
statusCode: number; | |
} |
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 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?
const defaultResult = await defaultIngestErrorHandler({ error: e, response }); | ||
if (e instanceof IngestManagerError) { |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on wether this is the right version I'm using here?
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.
yes, version is the currently installed 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.
Thanks!
oldVersion: latestPackage.version, | ||
assets: [ | ||
...installedPkg.attributes.installed_es, | ||
...installedPkg.attributes.installed_kibana, |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
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
@@ -95,6 +99,117 @@ export async function ensureInstalledPackage(options: { | |||
return installation; | |||
} | |||
|
|||
export async function handleInstallPackageFailure( | |||
savedObjectsClient: SavedObjectsClientContract, |
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.
Please convert this to an object. 6 parameters is about 2x my max.
@@ -14,10 +14,12 @@ export const FLEET_API_ROOT = `${API_ROOT}/fleet`; | |||
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
@@ -71,6 +71,26 @@ export interface InstallPackageResponse { | |||
response: AssetReference[]; | |||
} | |||
|
|||
export interface UpgradePackageError { |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah thanks!
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks John! I'll pull in your changes.
version: latestPackage.version, | ||
}); | ||
|
||
try { |
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.
Same comment re: Promise.allSettled
} | ||
|
||
export interface UpgradePackagesResponse { | ||
response: Array<UpgradePackageInfo | UpgradePackageError>; |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description missing a word I 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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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_bulk
to 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
endpoint
package 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.