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

[Ingest Manager] Adding bulk packages upgrade api #77827

Merged
merged 13 commits into from
Sep 22, 2020

Conversation

jonathan-buttner
Copy link
Contributor

@jonathan-buttner jonathan-buttner commented Sep 17, 2020

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: #77498

And add similar functionality in the Ingest Manager's UI to upgrade the default packages when a user is navigating between the pages.

@jonathan-buttner jonathan-buttner added v8.0.0 release_note:skip Skip the PR/issue when compiling release notes Feature:EPM Fleet team's Elastic Package Manager (aka Integrations) project v7.10.0 Team:Fleet Team label for Observability Data Collection Fleet team labels Sep 17, 2020
@jonathan-buttner jonathan-buttner requested review from jfsiii, neptunian and a team September 17, 2020 20:55
@elasticmachine
Copy link
Contributor

Pinging @elastic/ingest-management (Feature:EPM)

@elasticmachine
Copy link
Contributor

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`;
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Contributor

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_*.

Copy link
Contributor Author

@jonathan-buttner jonathan-buttner Sep 18, 2020

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?

Copy link
Contributor

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`

Copy link
Contributor Author

@jonathan-buttner jonathan-buttner Sep 18, 2020

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.

Copy link
Contributor

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

Copy link
Contributor Author

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?

Copy link
Contributor

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) {
Copy link
Contributor Author

@jonathan-buttner jonathan-buttner Sep 17, 2020

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.

Copy link
Contributor

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.

Copy link
Contributor

@jfsiii jfsiii Sep 17, 2020

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 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.

Copy link
Contributor

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'>

/**
* 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;
}

Copy link
Contributor Author

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) {
Copy link
Contributor Author

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.

}

// TODO I think we want version here and not `install_version`?
if (!installedPkg || semver.gt(latestPackage.version, installedPkg.attributes.version)) {
Copy link
Contributor Author

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?

Copy link
Contributor

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

Copy link
Contributor Author

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,
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

@jfsiii jfsiii left a 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,
Copy link
Contributor

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`;
Copy link
Contributor

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) {
Copy link
Contributor

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 {
Copy link
Contributor

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) {
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah thanks!

Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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 {
Copy link
Contributor

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>;
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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[];
}

@neptunian
Copy link
Contributor

It looks like this change will install the latest package if it doesn't exist. Which makes it similar to our current route /packages/{pkg} for installing/upgrading a single package. But we don't call it upgrade, we just call it install. I was thinking we should follow similar naming for bulk install/updating.

@jonathan-buttner
Copy link
Contributor Author

It looks like this change will install the latest package if it doesn't exist. Which makes it similar to our current route /packages/{pkg} for installing/upgrading a single package. But we don't call it upgrade, we just call it install. I was thinking we should follow similar naming for bulk install/updating.

Yeah good point, I can switch everything over to "install" instead of upgrade.

@neptunian
Copy link
Contributor

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 () {
Copy link
Contributor

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

Copy link
Contributor Author

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'] })
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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) {
Copy link
Contributor

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

@kibanamachine
Copy link
Contributor

💚 Build Succeeded

Build metrics

page load bundle size

id value diff baseline
ingestManager 514.5KB +192.0B 514.3KB

History

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

@jonathan-buttner jonathan-buttner merged commit 311805a into elastic:master Sep 22, 2020
@jonathan-buttner jonathan-buttner deleted the ingest-bulk-upgrade branch September 22, 2020 16:50
jonathan-buttner added a commit to jonathan-buttner/kibana that referenced this pull request Sep 22, 2020
* 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
jonathan-buttner added a commit that referenced this pull request Sep 22, 2020
* 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Feature:EPM Fleet team's Elastic Package Manager (aka Integrations) project release_note:skip Skip the PR/issue when compiling release notes Team:Fleet Team label for Observability Data Collection Fleet team v7.10.0 v8.0.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants