-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Fleet][EPM] Save installed package assets in ES (#83391)
## Summary Store package assets (from Registry or local upload) in Elasticsearch. Related to proposal [issue](#83426) & [document](https://docs.google.com/document/d/18XoS6CSl9UxxPPBt9LXuJngf1Jv-4tl3jY6l19U1yH8) * New `epm-packages-assets` saved objects are stored on `.kibana` index, like our existing saved object `epm-packages` * Asset id is uuid v5 based on the package name, package version & file path. See 1974324 * Add a list of IDs of all the installed assets, to `epm-packages` saved object. Like the existing `installed_` properties. [Example](https://github.com/elastic/kibana/pull/83391/files#diff-fa07cac51b6a49bf1e4824bc2250c9a77dac6c7d6b0a56020f559ef1ff9be25fR491-R512) from a test <details><summary>Mapping for new Saved Object</summary> https://github.com/elastic/kibana/blob/37f7b6ded747edb5cc487661b801c5e1c0a102a7/x-pack%2Fplugins%2Ffleet%2Fserver%2Fsaved_objects%2Findex.ts#L329-L339 </details> <details><summary>Additional property on existing <code>epm-packages</code> Saved Object</summary> https://github.com/elastic/kibana/blob/c4f27ab25715a225c710727a37f5f105364e2f72/x-pack/plugins/fleet/server/saved_objects/index.ts#L306-L312 I don't think the saved object changes are strictly required. It can be removed without changing much about how things work - Pros: - Preserves accurate record of the assets added at installation time. Separates what assets are currently available for package-version from what was installed. They _should_ be the same, but things happen. - Avoids a query to get the installed assets before operating on them - Cons: - size/noise? Could be tens or hundreds of ids - migration? </details> ### More details **When are saved objects added?** During installation, after all other actions have succeeded, just before marking the save object as installed, we commit all the files from the package to ES https://github.com/elastic/kibana/blob/37f7b6ded747edb5cc487661b801c5e1c0a102a7/x-pack%2Fplugins%2Ffleet%2Fserver%2Fservices%2Fepm%2Fpackages%2F_install_package.ts#L193-L198 **When are documents removed from the index?** In the `removeInstallation` function which is called in response to a `DELETE /api/fleet/epm/packages/pkgkey` https://github.com/elastic/kibana/blob/37f7b6ded747edb5cc487661b801c5e1c0a102a7/x-pack%2Fplugins%2Ffleet%2Fserver%2Fservices%2Fepm%2Fpackages%2Fremove.ts#L72 or a failed package (re-)installation https://github.com/elastic/kibana/blob/bf068739acce044ac27902253e8fc31df621f081/x-pack%2Fplugins%2Ffleet%2Fserver%2Fservices%2Fepm%2Fpackages%2Finstall.ts#L145 **How are we using these assets?** We're not, currently. Here's an example showing how we could update [`getFileHandler`](https://github.com/elastic/kibana/blob/514b50e4c2d7a3be79d77e73838ff57b6cf1304a/x-pack%2Fplugins%2Ffleet%2Fserver%2Froutes%2Fepm%2Fhandlers.ts#L101) to check for local assets before reaching out to the Registry if we wished. It's not DRY, but it does work ```typescript const esDocRoot = `http://elastic:changeme@localhost:9200/${PACKAGE_ASSETS_INDEX_NAME}/_doc`; const escapedDocId = encodeURIComponent(`${pkgName}-${pkgVersion}/${filePath}`); const esRes = await fetch(`${esDocRoot}/${escapedDocId}`); const esJson = await esRes.json(); if (esJson.found) { const asset: PackageAsset = esJson._source; const body = asset.data_utf8 || Buffer.from(asset.data_base64, 'base64'); return response.ok({ body, headers: { 'content-type': asset.media_type, // should add our own `cache-control` header here // kibana default is prevents caching: `private, no-cache, no-store, must-revalidate` // #83631 }, }); } ``` ### Checklist _updated tests to include new saved object output, no tests added yet_ - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
- Loading branch information
John Schulz
authored
Dec 7, 2020
1 parent
f961e90
commit 81a340e
Showing
13 changed files
with
232 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
x-pack/plugins/fleet/server/services/epm/archive/save_to_es.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { extname } from 'path'; | ||
import { isBinaryFile } from 'isbinaryfile'; | ||
import mime from 'mime-types'; | ||
import uuidv5 from 'uuid/v5'; | ||
import { SavedObjectsClientContract, SavedObjectsBulkCreateObject } from 'src/core/server'; | ||
import { | ||
ASSETS_SAVED_OBJECT_TYPE, | ||
InstallablePackage, | ||
InstallSource, | ||
PackageAssetReference, | ||
} from '../../../../common'; | ||
import { getArchiveEntry } from './index'; | ||
|
||
// uuid v5 requires a SHA-1 UUID as a namespace | ||
// used to ensure same input produces the same id | ||
const ID_NAMESPACE = '71403015-cdd5-404b-a5da-6c43f35cad84'; | ||
|
||
// could be anything, picked this from https://github.com/elastic/elastic-agent-client/issues/17 | ||
const MAX_ES_ASSET_BYTES = 4 * 1024 * 1024; | ||
|
||
export interface PackageAsset { | ||
package_name: string; | ||
package_version: string; | ||
install_source: string; | ||
asset_path: string; | ||
media_type: string; | ||
data_utf8: string; | ||
data_base64: string; | ||
} | ||
|
||
export async function archiveEntryToESDocument(opts: { | ||
path: string; | ||
buffer: Buffer; | ||
name: string; | ||
version: string; | ||
installSource: InstallSource; | ||
}): Promise<PackageAsset> { | ||
const { path, buffer, name, version, installSource } = opts; | ||
const fileExt = extname(path); | ||
const contentType = mime.lookup(fileExt); | ||
const mediaType = mime.contentType(contentType || fileExt); | ||
// can use to create a data URL like `data:${mediaType};base64,${base64Data}` | ||
|
||
const bufferIsBinary = await isBinaryFile(buffer); | ||
const dataUtf8 = bufferIsBinary ? '' : buffer.toString('utf8'); | ||
const dataBase64 = bufferIsBinary ? buffer.toString('base64') : ''; | ||
|
||
// validation: filesize? asset type? anything else | ||
if (dataUtf8.length > MAX_ES_ASSET_BYTES) { | ||
throw new Error(`File at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}`); | ||
} | ||
|
||
if (dataBase64.length > MAX_ES_ASSET_BYTES) { | ||
throw new Error( | ||
`After base64 encoding file at ${path} is larger than maximum allowed size of ${MAX_ES_ASSET_BYTES}` | ||
); | ||
} | ||
|
||
return { | ||
package_name: name, | ||
package_version: version, | ||
install_source: installSource, | ||
asset_path: path, | ||
media_type: mediaType || '', | ||
data_utf8: dataUtf8, | ||
data_base64: dataBase64, | ||
}; | ||
} | ||
|
||
export async function removeArchiveEntries(opts: { | ||
savedObjectsClient: SavedObjectsClientContract; | ||
refs: PackageAssetReference[]; | ||
}) { | ||
const { savedObjectsClient, refs } = opts; | ||
const results = await Promise.all( | ||
refs.map((ref) => savedObjectsClient.delete(ASSETS_SAVED_OBJECT_TYPE, ref.id)) | ||
); | ||
return results; | ||
} | ||
|
||
export async function saveArchiveEntries(opts: { | ||
savedObjectsClient: SavedObjectsClientContract; | ||
paths: string[]; | ||
packageInfo: InstallablePackage; | ||
installSource: InstallSource; | ||
}) { | ||
const { savedObjectsClient, paths, packageInfo, installSource } = opts; | ||
const bulkBody = await Promise.all( | ||
paths.map((path) => { | ||
const buffer = getArchiveEntry(path); | ||
if (!buffer) throw new Error(`Could not find ArchiveEntry at ${path}`); | ||
const { name, version } = packageInfo; | ||
return archiveEntryToBulkCreateObject({ path, buffer, name, version, installSource }); | ||
}) | ||
); | ||
|
||
const results = await savedObjectsClient.bulkCreate<PackageAsset>(bulkBody); | ||
return results; | ||
} | ||
|
||
export async function archiveEntryToBulkCreateObject(opts: { | ||
path: string; | ||
buffer: Buffer; | ||
name: string; | ||
version: string; | ||
installSource: InstallSource; | ||
}): Promise<SavedObjectsBulkCreateObject<PackageAsset>> { | ||
const { path, buffer, name, version, installSource } = opts; | ||
const doc = await archiveEntryToESDocument({ path, buffer, name, version, installSource }); | ||
return { | ||
id: uuidv5(doc.asset_path, ID_NAMESPACE), | ||
type: ASSETS_SAVED_OBJECT_TYPE, | ||
attributes: doc, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters