-
Notifications
You must be signed in to change notification settings - Fork 8.2k
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
[EPM] Make API consistent for package installation and removal #48745
Changes from all commits
410465e
8dbd67e
8809b55
92848d8
fc737b1
51eddcb
7d9389b
3842acb
5a218a5
cbb8328
de72683
cb48028
9c97d46
2df30c5
b6c8d94
dcb32b3
dd1ce82
d25d942
5a1b73c
bfe6a21
d365f20
efef740
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,14 +10,15 @@ export { Request, ResponseToolkit, Server, ServerRoute } from 'hapi'; | |
|
||
export type InstallationStatus = Installed['status'] | NotInstalled['status']; | ||
|
||
export type AssetType = | ||
| 'config' | ||
| 'dashboard' | ||
| 'index-pattern' | ||
| 'ingest-pipeline' | ||
| 'search' | ||
| 'timelion-sheet' | ||
| 'visualization'; | ||
export enum AssetType { | ||
config = 'config', | ||
dashboard = 'dashboard', | ||
visualization = 'visualization', | ||
search = 'search', | ||
ingestPipeline = 'ingest-pipeline', | ||
indexPattern = 'index-pattern', | ||
timelionSheet = 'timelion-sheet', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not for this PR but more general thought on this: The reason I prefixed the asset always by the service is to prevent conflicts in the future and make them unique. So if there is an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. At the moment some of these double as Kibana saved-object types (as defined here: https://www.elastic.co/guide/en/kibana/master/saved-objects-api-get.html) so we'll probably want to uncouple our asset types from those and only map between them. |
||
} | ||
|
||
// Registry's response types | ||
// from /search | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,24 +27,19 @@ interface ListPackagesRequest extends Request { | |
query: Request['query'] & SearchParams; | ||
} | ||
|
||
interface PackageRequest extends Request { | ||
interface PackageInfoRequest extends Request { | ||
params: { | ||
pkgkey: string; | ||
}; | ||
} | ||
|
||
interface InstallAssetRequest extends Request { | ||
params: AssetRequestParams; | ||
} | ||
|
||
interface DeleteAssetRequest extends Request { | ||
params: AssetRequestParams; | ||
interface InstallDeletePackageRequest extends Request { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I liked that these were separate and am 50/50 on the name but we're not exporting and we can always split them up later. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, the important change here is |
||
params: { | ||
pkgkey: string; | ||
asset: AssetType; | ||
}; | ||
} | ||
|
||
type AssetRequestParams = PackageRequest['params'] & { | ||
asset?: AssetType; | ||
}; | ||
|
||
export async function handleGetCategories(req: Request, extra: Extra) { | ||
return getCategories(); | ||
} | ||
|
@@ -59,7 +54,7 @@ export async function handleGetList(req: ListPackagesRequest, extra: Extra) { | |
return packageList; | ||
} | ||
|
||
export async function handleGetInfo(req: PackageRequest, extra: Extra) { | ||
export async function handleGetInfo(req: PackageInfoRequest, extra: Extra) { | ||
const { pkgkey } = req.params; | ||
const savedObjectsClient = getClient(req); | ||
const packageInfo = await getPackageInfo({ savedObjectsClient, pkgkey }); | ||
|
@@ -81,26 +76,20 @@ export const handleGetFile = async (req: Request, extra: Extra) => { | |
return epmResponse; | ||
}; | ||
|
||
export async function handleRequestInstall(req: InstallAssetRequest, extra: Extra) { | ||
const { pkgkey, asset } = req.params; | ||
if (!asset) throw new Error('Unhandled empty/default asset case'); | ||
|
||
export async function handleRequestInstall(req: InstallDeletePackageRequest, extra: Extra) { | ||
const { pkgkey } = req.params; | ||
const savedObjectsClient = getClient(req); | ||
const callCluster = getClusterAccessor(extra.context.esClient, req); | ||
const object = await installPackage({ | ||
return await installPackage({ | ||
savedObjectsClient, | ||
pkgkey, | ||
asset, | ||
callCluster, | ||
}); | ||
|
||
return object; | ||
} | ||
|
||
export async function handleRequestDelete(req: DeleteAssetRequest, extra: Extra) { | ||
export async function handleRequestDelete(req: InstallDeletePackageRequest, extra: Extra) { | ||
const { pkgkey } = req.params; | ||
const savedObjectsClient = getClient(req); | ||
const deleted = await removeInstallation({ savedObjectsClient, pkgkey }); | ||
const callCluster = getClusterAccessor(extra.context.esClient, req); | ||
const deleted = await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); | ||
|
||
return deleted; | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -8,35 +8,30 @@ import { SavedObject, SavedObjectsClientContract } from 'src/core/server/'; | |||||
import { SAVED_OBJECT_TYPE } from '../../common/constants'; | ||||||
import { AssetReference, AssetType, InstallationAttributes } from '../../common/types'; | ||||||
import * as Registry from '../registry'; | ||||||
import { CallESAsCurrentUser, assetUsesObjects, getInstallationObject } from './index'; | ||||||
import { getInstallationObject, getPackageInfo } from './index'; | ||||||
import { getObjects } from './get_objects'; | ||||||
|
||||||
export async function installPackage(options: { | ||||||
savedObjectsClient: SavedObjectsClientContract; | ||||||
pkgkey: string; | ||||||
asset: AssetType; | ||||||
callCluster: CallESAsCurrentUser; | ||||||
}) { | ||||||
const { savedObjectsClient, pkgkey, asset, callCluster } = options; | ||||||
// install any assets (in ES, as Saved Objects, etc) as required. Get references to them | ||||||
const { savedObjectsClient, pkgkey } = options; | ||||||
|
||||||
const toSave = await installAssets({ | ||||||
savedObjectsClient, | ||||||
pkgkey, | ||||||
asset, | ||||||
callCluster, | ||||||
}); | ||||||
|
||||||
if (toSave.length) { | ||||||
// saved those references in the package manager's state object | ||||||
const saved = await saveInstallationReferences({ | ||||||
// Save those references in the integration manager's state saved object | ||||||
await saveInstallationReferences({ | ||||||
savedObjectsClient, | ||||||
pkgkey, | ||||||
toSave, | ||||||
}); | ||||||
return saved; | ||||||
} | ||||||
|
||||||
return []; | ||||||
return getPackageInfo({ savedObjectsClient, pkgkey }); | ||||||
} | ||||||
|
||||||
// the function which how to install each of the various asset types | ||||||
|
@@ -45,20 +40,20 @@ export async function installPackage(options: { | |||||
export async function installAssets(options: { | ||||||
savedObjectsClient: SavedObjectsClientContract; | ||||||
pkgkey: string; | ||||||
asset: AssetType; | ||||||
callCluster: CallESAsCurrentUser; | ||||||
}) { | ||||||
const { savedObjectsClient, pkgkey, asset, callCluster } = options; | ||||||
if (assetUsesObjects(asset)) { | ||||||
const references = await installObjects({ savedObjectsClient, pkgkey, asset }); | ||||||
return references; | ||||||
} | ||||||
if (asset === 'ingest-pipeline') { | ||||||
const references = await installPipelines({ callCluster, pkgkey }); | ||||||
return references; | ||||||
} | ||||||
const { savedObjectsClient, pkgkey } = options; | ||||||
|
||||||
// Only install certain Kibana assets during package installation. | ||||||
// All other asset types need special handling | ||||||
const typesToInstall = [AssetType.visualization, AssetType.dashboard, AssetType.search]; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these 3 different than the other
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, they are different. I only included those Kibana assets here of which I'm more or less sure, for now, that we can just copy them over. Index patterns will need to be generated from |
||||||
|
||||||
const installationPromises = typesToInstall.map(async assetType => | ||||||
installKibanaSavedObjects({ savedObjectsClient, pkgkey, assetType }) | ||||||
); | ||||||
|
||||||
return []; | ||||||
// installKibanaSavedObjects returns AssetReference[], so .map creates AssetReference[][] | ||||||
// call .flat to flatten into one dimensional array | ||||||
return Promise.all(installationPromises).then(results => results.flat()); | ||||||
} | ||||||
|
||||||
export async function saveInstallationReferences(options: { | ||||||
|
@@ -85,57 +80,29 @@ export async function saveInstallationReferences(options: { | |||||
return results; | ||||||
} | ||||||
|
||||||
async function installObjects({ | ||||||
async function installKibanaSavedObjects({ | ||||||
savedObjectsClient, | ||||||
pkgkey, | ||||||
asset, | ||||||
assetType, | ||||||
}: { | ||||||
savedObjectsClient: SavedObjectsClientContract; | ||||||
pkgkey: string; | ||||||
asset: AssetType; | ||||||
}) { | ||||||
const isSameType = ({ path }: Registry.ArchiveEntry) => asset === Registry.pathParts(path).type; | ||||||
const toBeSavedObjects = await getObjects(pkgkey, isSameType); | ||||||
const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { overwrite: true }); | ||||||
const createdObjects = createResults.saved_objects; | ||||||
const installed = createdObjects.map(toAssetReference); | ||||||
|
||||||
return installed; | ||||||
} | ||||||
|
||||||
async function installPipelines({ | ||||||
callCluster, | ||||||
pkgkey, | ||||||
}: { | ||||||
callCluster: CallESAsCurrentUser; | ||||||
pkgkey: string; | ||||||
assetType: AssetType; | ||||||
}) { | ||||||
const isPipeline = ({ path }: Registry.ArchiveEntry) => | ||||||
Registry.pathParts(path).type === 'ingest-pipeline'; | ||||||
const paths = await Registry.getArchiveInfo(pkgkey, isPipeline); | ||||||
const installationPromises = paths.map(path => installPipeline({ callCluster, path })); | ||||||
const references = await Promise.all(installationPromises); | ||||||
|
||||||
return references; | ||||||
} | ||||||
const isSameType = ({ path }: Registry.ArchiveEntry) => | ||||||
assetType === Registry.pathParts(path).type; | ||||||
|
||||||
async function installPipeline({ | ||||||
callCluster, | ||||||
path, | ||||||
}: { | ||||||
callCluster: CallESAsCurrentUser; | ||||||
path: string; | ||||||
}): Promise<AssetReference> { | ||||||
const buffer = Registry.getAsset(path); | ||||||
// sample data is invalid json. strip the offending parts before parsing | ||||||
const json = buffer.toString('utf8').replace(/\\/g, ''); | ||||||
const pipeline = JSON.parse(json); | ||||||
const { file, type } = Registry.pathParts(path); | ||||||
const id = file.replace('.json', ''); | ||||||
// TODO: any sort of error, not "happy path", handling | ||||||
await callCluster('ingest.putPipeline', { id, body: pipeline }); | ||||||
|
||||||
return { id, type }; | ||||||
const toBeSavedObjects = await getObjects(pkgkey, isSameType); | ||||||
if (toBeSavedObjects.length === 0) { | ||||||
return []; | ||||||
} else { | ||||||
const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { | ||||||
overwrite: true, | ||||||
}); | ||||||
const createdObjects = createResults.saved_objects; | ||||||
const installed = createdObjects.map(toAssetReference); | ||||||
return installed; | ||||||
} | ||||||
} | ||||||
|
||||||
function toAssetReference({ id, type }: SavedObject) { | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,13 +6,20 @@ | |
|
||
import { SavedObjectsClientContract } from 'src/core/server/'; | ||
import { SAVED_OBJECT_TYPE } from '../../common/constants'; | ||
import { getInstallationObject } from './index'; | ||
import { | ||
getInstallationObject, | ||
assetUsesObjects, | ||
CallESAsCurrentUser, | ||
getPackageInfo, | ||
} from './index'; | ||
import { AssetType } from '../../common/types'; | ||
|
||
export async function removeInstallation(options: { | ||
savedObjectsClient: SavedObjectsClientContract; | ||
pkgkey: string; | ||
callCluster: CallESAsCurrentUser; | ||
}) { | ||
const { savedObjectsClient, pkgkey } = options; | ||
const { savedObjectsClient, pkgkey, callCluster } = options; | ||
const installation = await getInstallationObject({ savedObjectsClient, pkgkey }); | ||
const installedObjects = (installation && installation.attributes.installed) || []; | ||
|
||
|
@@ -21,11 +28,24 @@ export async function removeInstallation(options: { | |
await savedObjectsClient.delete(SAVED_OBJECT_TYPE, pkgkey); | ||
|
||
// Delete the installed assets | ||
const deletePromises = installedObjects.map(async ({ id, type }) => | ||
savedObjectsClient.delete(type, id) | ||
); | ||
const deletePromises = installedObjects.map(async ({ id, type }) => { | ||
if (assetUsesObjects(type as AssetType)) { | ||
savedObjectsClient.delete(type, id); | ||
} else if (type === AssetType.ingestPipeline) { | ||
deletePipeline(callCluster, id); | ||
} | ||
}); | ||
await Promise.all(deletePromises); | ||
|
||
// successful delete's in SO client return {}. return something more useful | ||
return installedObjects; | ||
const packageInfo = await getPackageInfo({ savedObjectsClient, pkgkey }); | ||
|
||
return packageInfo; | ||
} | ||
|
||
async function deletePipeline(callCluster: CallESAsCurrentUser, id: string): Promise<void> { | ||
// '*' shouldn't ever appear here, but it still would delete all ingest pipelines | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we do something (log, error, etc) when we get an unexpected value? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should. I'll open an issue. |
||
if (id && id !== '*') { | ||
await callCluster('ingest.deletePipeline', { 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.
I hope this works. It's what I wanted to do initially, but had some challenges. I think I've both fixed the things that were getting in the way and have a better hand on TS but I'll run locally to confirm. 🤞
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.
Ran locally and things still work as expected. I was struggling with a way to have these strings as both a union type & some way to do "lookup" for the "does type x use saved objects?" question. I ended up using a
Set
forSAVED_OBJECT_TYPES
and we can create that from a few different structures (this PR shows that) so seems ok.Can you talk about why you changed it to an
enum
? Is it solely to be able to use code likeAssetType.config
(and have one linked property name vs N duplicated strings) or are there other benefits?FWIW, I think I'm ok changing to this. I'm just documenting what I can remember about why I went one way, and I'd like to know more about the benefits and trade-offs. I'm still new to TS & enum so I don't have practical experience with any benefits/drawbacks.
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 wanted to be able to iterate over them, even if in the end I didn't need to. Also, for me it feels like the correct type -- we use them in the metrics UI for things like node types so I got used to using them there.