diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index b0277b79eca6b..e0f2ae0f4f4ba 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -307,6 +307,7 @@ "apiKey" ], "epm-packages": [ + "additional_spaces_installed_kibana", "es_index_patterns", "experimental_data_stream_features", "experimental_data_stream_features.data_stream", diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index e0b84e572e0f9..778ed3c37992c 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1041,6 +1041,10 @@ }, "epm-packages": { "properties": { + "additional_spaces_installed_kibana": { + "index": false, + "type": "flattened" + }, "es_index_patterns": { "dynamic": false, "properties": {} diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index d318b96435469..a5b264882ca3d 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -90,7 +90,7 @@ describe('checking migration metadata changes on all registered SO types', () => "enterprise_search_telemetry": "9ac912e1417fc8681e0cd383775382117c9e3d3d", "entity-definition": "33fe0194bd896f0bfe479d55f6de20f8ba1d7713", "entity-discovery-api-key": "c267a65c69171d1804362155c1378365f5acef88", - "epm-packages": "f8ee125b57df31fd035dc04ad81aef475fd2f5bd", + "epm-packages": "8042d4a1522f6c4e6f5486e791b3ffe3a22f88fd", "epm-packages-assets": "7a3e58efd9a14191d0d1a00b8aaed30a145fd0b1", "event-annotation-group": "715ba867d8c68f3c9438052210ea1c30a9362582", "event_loop_delays_daily": "01b967e8e043801357503de09199dfa3853bab88", diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts index ee775ff1dbdd8..d114cc3975cbf 100644 --- a/x-pack/plugins/fleet/common/constants/routes.ts +++ b/x-pack/plugins/fleet/common/constants/routes.ts @@ -36,6 +36,8 @@ export const EPM_API_ROUTES = { INSTALL_BY_UPLOAD_PATTERN: EPM_PACKAGES_MANY, CUSTOM_INTEGRATIONS_PATTERN: `${EPM_API_ROOT}/custom_integrations`, DELETE_PATTERN: EPM_PACKAGES_ONE, + INSTALL_KIBANA_ASSETS_PATTERN: `${EPM_PACKAGES_ONE}/kibana_assets`, + DELETE_KIBANA_ASSETS_PATTERN: `${EPM_PACKAGES_ONE}/kibana_assets`, FILEPATH_PATTERN: `${EPM_PACKAGES_ONE}/{filePath*}`, CATEGORIES_PATTERN: `${EPM_API_ROOT}/categories`, VERIFICATION_KEY_ID: `${EPM_API_ROOT}/verification_key_id`, diff --git a/x-pack/plugins/fleet/common/experimental_features.ts b/x-pack/plugins/fleet/common/experimental_features.ts index 6233ef5f820cf..5e8679e555908 100644 --- a/x-pack/plugins/fleet/common/experimental_features.ts +++ b/x-pack/plugins/fleet/common/experimental_features.ts @@ -7,11 +7,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues; -/** - * A list of allowed values that can be used in `xpack.fleet.enableExperimental`. - * This object is then used to validate and parse the value entered. - */ -export const allowedExperimentalValues = Object.freeze>({ +const _allowedExperimentalValues = { createPackagePolicyMultiPageLayout: true, packageVerification: true, showDevtoolsRequest: true, @@ -32,9 +28,18 @@ export const allowedExperimentalValues = Object.freeze>( advancedPolicySettings: true, useSpaceAwareness: false, enableReusableIntegrationPolicies: false, -}); +}; + +/** + * A list of allowed values that can be used in `xpack.fleet.enableExperimental`. + * This object is then used to validate and parse the value entered. + */ +export const allowedExperimentalValues = Object.freeze< + Record +>({ ..._allowedExperimentalValues }); -type ExperimentalConfigKeys = Array; +type ExperimentalConfigKey = keyof ExperimentalFeatures; +type ExperimentalConfigKeys = ExperimentalConfigKey[]; type Mutable = { -readonly [P in keyof T]: T[P] }; const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly; @@ -46,7 +51,7 @@ const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly { - const enabledFeatures: Mutable = {}; + const enabledFeatures: Mutable = { ...allowedExperimentalValues }; for (const value of configValue) { if (isValidExperimentalValue(value)) { @@ -60,8 +65,8 @@ export const parseExperimentalConfigValue = (configValue: string[]): Experimenta }; }; -export const isValidExperimentalValue = (value: string) => { - return allowedKeys.includes(value); +export const isValidExperimentalValue = (value: string): value is ExperimentalConfigKey => { + return (allowedKeys as string[]).includes(value); }; export const getExperimentalAllowedValues = (): string[] => [...allowedKeys]; diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts index 76b963949699a..260653283823d 100644 --- a/x-pack/plugins/fleet/common/services/routes.ts +++ b/x-pack/plugins/fleet/common/services/routes.ts @@ -78,6 +78,12 @@ export const epmRouteService = { .replace(/\/$/, ''); // trim trailing slash }, + getInstallKibanaAssetsPath: (pkgName: string, pkgVersion: string) => { + return EPM_API_ROUTES.INSTALL_KIBANA_ASSETS_PATTERN.replace('{pkgName}', pkgName) + .replace('{pkgVersion}', pkgVersion) + .replace(/\/$/, ''); // trim trailing slash + }, + getUpdatePath: (pkgName: string, pkgVersion: string) => { return EPM_API_ROUTES.INFO_PATTERN.replace('{pkgName}', pkgName).replace( '{pkgVersion}', diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 5c5a48642cba8..06a8b979c8eb5 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -589,6 +589,7 @@ export interface StateContext { export interface Installation { installed_kibana: KibanaAssetReference[]; + additional_spaces_installed_kibana?: Record; installed_es: EsAssetReference[]; package_assets?: PackageAssetReference[]; es_index_patterns: Record; @@ -649,6 +650,7 @@ export type AssetReference = KibanaAssetReference | EsAssetReference; export interface KibanaAssetReference { id: string; + originId?: string; type: KibanaSavedObjectType; } export interface EsAssetReference { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx index f79a5ad00c67a..9b235df27b3c9 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.test.tsx @@ -680,7 +680,7 @@ describe('When on the package policy create page', () => { isServerlessEnabled: true, }, }); - jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: true }); + jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: true } as any); (useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue( getMockPackageInfo({ requiresRoot: false, dataStreamRequiresRoot: false }) ); @@ -703,7 +703,7 @@ describe('When on the package policy create page', () => { }); test('should not force create package policy when not in serverless', async () => { - jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: false }); + jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: false } as any); (useStartServices as jest.MockedFunction).mockReturnValue({ ...useStartServices(), cloud: { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx index 5075d532dd3b1..c57d26603c889 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/settings/components/edit_output_flyout/index.test.tsx @@ -192,7 +192,7 @@ describe('EditOutputFlyout', () => { it('should populate secret input with plain text value when editing kafka output', async () => { jest .spyOn(ExperimentalFeaturesService, 'get') - .mockReturnValue({ outputSecretsStorage: true, kafkaOutput: true }); + .mockReturnValue({ outputSecretsStorage: true, kafkaOutput: true } as any); mockedUseFleetStatus.mockReturnValue({ isLoading: false, @@ -232,7 +232,7 @@ describe('EditOutputFlyout', () => { it('should populate secret password input with plain text value when editing kafka output', async () => { jest .spyOn(ExperimentalFeaturesService, 'get') - .mockReturnValue({ outputSecretsStorage: true, kafkaOutput: true }); + .mockReturnValue({ outputSecretsStorage: true, kafkaOutput: true } as any); mockedUseFleetStatus.mockReturnValue({ isLoading: false, @@ -273,7 +273,9 @@ describe('EditOutputFlyout', () => { }); it('should populate secret input with plain text value when editing logstash output', async () => { - jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ outputSecretsStorage: true }); + jest + .spyOn(ExperimentalFeaturesService, 'get') + .mockReturnValue({ outputSecretsStorage: true } as any); mockedUseFleetStatus.mockReturnValue({ isLoading: false, @@ -325,7 +327,7 @@ describe('EditOutputFlyout', () => { it('should render the flyout if the output provided is a remote ES output', async () => { jest .spyOn(ExperimentalFeaturesService, 'get') - .mockReturnValue({ remoteESOutput: true, outputSecretsStorage: true }); + .mockReturnValue({ remoteESOutput: true, outputSecretsStorage: true } as any); mockedUseFleetStatus.mockReturnValue({ isLoading: false, @@ -356,7 +358,7 @@ describe('EditOutputFlyout', () => { it('should populate secret service token input with plain text value when editing remote ES output', async () => { jest .spyOn(ExperimentalFeaturesService, 'get') - .mockReturnValue({ remoteESOutput: true, outputSecretsStorage: true }); + .mockReturnValue({ remoteESOutput: true, outputSecretsStorage: true } as any); mockedUseFleetStatus.mockReturnValue({ isLoading: false, @@ -392,7 +394,7 @@ describe('EditOutputFlyout', () => { }); it('should not display remote ES output in type lists if serverless', async () => { - jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ remoteESOutput: true }); + jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ remoteESOutput: true } as any); mockUseStartServices.mockReset(); mockStartServices(true); const { utils } = renderFlyout({ diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx index c8f60a35c9039..e9ae0c148a180 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx @@ -10,6 +10,8 @@ import { Redirect } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiTitle, EuiCallOut } from '@elastic/eui'; +import { ExperimentalFeaturesService } from '../../../../../../../services'; + import type { EsAssetReference, AssetSOObject, @@ -31,12 +33,11 @@ import { useAuthz, useFleetStatus, } from '../../../../../hooks'; - import { sendGetBulkAssets } from '../../../../../hooks'; import { DeferredAssetsSection } from './deferred_assets_accordion'; - import { AssetsAccordion } from './assets_accordion'; +import { InstallKibanaAssetsButton } from './install_kibana_assets_button'; interface AssetsPanelProps { packageInfo: PackageInfo; @@ -50,6 +51,8 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps const { docLinks } = useStartServices(); const { spaceId } = useFleetStatus(); + const { useSpaceAwareness } = ExperimentalFeaturesService.get(); + const customAssetsExtension = useUIExtension(packageInfo.name, 'package-detail-assets'); const canReadPackageSettings = useAuthz().integrations.readPackageInfo; @@ -62,23 +65,30 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps 'installationInfo' in packageInfo ? packageInfo.installationInfo : undefined; const installedSpaceId = pkgInstallationInfo?.installed_kibana_space_id; - const assetsInstalledInCurrentSpace = !installedSpaceId || installedSpaceId === spaceId; - + const assetsInstalledInCurrentSpace = + !installedSpaceId || + installedSpaceId === spaceId || + pkgInstallationInfo?.additional_spaces_installed_kibana?.[spaceId || 'default']; const [assetSavedObjectsByType, setAssetsSavedObjectsByType] = useState< Record> >({}); const [deferredInstallations, setDeferredInstallations] = useState(); + + const kibanaAssets = useMemo(() => { + return !installedSpaceId || installedSpaceId === spaceId + ? pkgInstallationInfo?.installed_kibana || [] + : pkgInstallationInfo?.additional_spaces_installed_kibana?.[spaceId || 'default'] || []; + }, [ + installedSpaceId, + spaceId, + pkgInstallationInfo?.installed_kibana, + pkgInstallationInfo?.additional_spaces_installed_kibana, + ]); const pkgAssets = useMemo( - () => [ - ...(assetsInstalledInCurrentSpace ? pkgInstallationInfo?.installed_kibana || [] : []), - ...(pkgInstallationInfo?.installed_es || []), - ], - [ - assetsInstalledInCurrentSpace, - pkgInstallationInfo?.installed_es, - pkgInstallationInfo?.installed_kibana, - ] + () => [...kibanaAssets, ...(pkgInstallationInfo?.installed_es || [])], + [kibanaAssets, pkgInstallationInfo?.installed_es] ); + const pkgAssetsByType = useMemo( () => pkgAssets.reduce((acc, asset) => { @@ -231,6 +241,13 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps }} />

+ {useSpaceAwareness ? ( + + ) : null} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/install_kibana_assets_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/install_kibana_assets_button.tsx new file mode 100644 index 0000000000000..d56b39f0c0bcd --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/install_kibana_assets_button.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiButton } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { i18n } from '@kbn/i18n'; + +import type { InstallationInfo } from '../../../../../../../../common/types'; +import { useAuthz, useInstallKibanaAssetsMutation, useStartServices } from '../../../../../hooks'; + +interface InstallKibanaAssetsButtonProps { + title: string; + installInfo: InstallationInfo; + onSuccess?: () => void; +} + +export function InstallKibanaAssetsButton({ + installInfo, + title, + onSuccess, +}: InstallKibanaAssetsButtonProps) { + const { notifications } = useStartServices(); + const { name, version } = installInfo; + const canInstallPackages = useAuthz().integrations.installPackages; + const { mutateAsync, isLoading } = useInstallKibanaAssetsMutation(); + + const handleClickInstall = useCallback(async () => { + try { + await mutateAsync({ + pkgName: name, + pkgVersion: version, + }); + if (onSuccess) { + await onSuccess(); + } + } catch (err) { + notifications.toasts.addError(err, { + title: i18n.translate('xpack.fleet.fleetServerSetup.kibanaInstallAssetsErrorTitle', { + defaultMessage: 'Error installing Kibana assets', + }), + }); + } + }, [mutateAsync, onSuccess, name, version, notifications.toasts]); + + return ( + + {isLoading ? ( + + ) : ( + + )} + + ); +} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx index 77cfdb3a9102e..40c865f8ad4d8 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.test.tsx @@ -25,7 +25,7 @@ const getHref = (k: string) => k; describe('Card utils', () => { describe('mapToCard', () => { beforeEach(() => { - ExperimentalFeaturesService.init({}); + ExperimentalFeaturesService.init({} as any); }); it('should use the installed version if available, without prelease', () => { diff --git a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts index 03bf36da75763..bd4bec9be6a1a 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; @@ -289,6 +289,11 @@ interface UpdatePackageArgs { body: UpdatePackageRequest['body']; } +interface InstallKibanaAssetsArgs { + pkgName: string; + pkgVersion: string; +} + export const useUpdatePackageMutation = () => { return useMutation( ({ pkgName, pkgVersion, body }: UpdatePackageArgs) => @@ -301,6 +306,22 @@ export const useUpdatePackageMutation = () => { ); }; +export const useInstallKibanaAssetsMutation = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ pkgName, pkgVersion }: InstallKibanaAssetsArgs) => + sendRequestForRq({ + path: epmRouteService.getInstallKibanaAssetsPath(pkgName, pkgVersion), + method: 'post', + version: API_VERSIONS.public.v1, + }), + onSuccess: (data, { pkgName, pkgVersion }) => { + return queryClient.invalidateQueries([pkgName, pkgVersion]); + }, + }); +}; + export const sendUpdatePackage = ( pkgName: string, pkgVersion: string, diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 709bd7b362a9f..c98adeb6f737f 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -340,6 +340,7 @@ export const installPackageFromRegistryHandler: FleetRequestHandler< return await defaultFleetErrorHandler({ error: res.error, response }); } }; + export const createCustomIntegrationHandler: FleetRequestHandler< undefined, undefined, @@ -640,6 +641,7 @@ const soToInstallationInfo = (pkg: PackageListItem | PackageInfo) => { ...pick(pkg.savedObject, ['created_at', 'updated_at', 'namespaces', 'type']), installed_kibana: attributes.installed_kibana, installed_kibana_space_id: attributes.installed_kibana_space_id, + additional_spaces_installed_kibana: attributes.additional_spaces_installed_kibana, installed_es: attributes.installed_es, install_status: attributes.install_status, install_source: attributes.install_source, diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index 3b7260c79aa7f..8f62dbe88d6a6 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -7,6 +7,8 @@ import type { IKibanaResponse } from '@kbn/core/server'; +import { parseExperimentalConfigValue } from '../../../common/experimental_features'; + import { API_VERSIONS } from '../../../common/constants'; import type { FleetAuthz } from '../../../common'; @@ -48,7 +50,10 @@ import { GetDataStreamsRequestSchema, CreateCustomIntegrationRequestSchema, GetInputsRequestSchema, + InstallKibanaAssetsRequestSchema, + DeleteKibanaAssetsRequestSchema, } from '../../types'; +import type { FleetConfigType } from '../../config'; import { getCategoriesHandler, @@ -70,6 +75,10 @@ import { getInputsHandler, } from './handlers'; import { getFileHandler } from './file_handler'; +import { + deletePackageKibanaAssetsHandler, + installPackageKibanaAssetsHandler, +} from './kibana_assets_handler'; const MAX_FILE_SIZE_BYTES = 104857600; // 100MB @@ -81,7 +90,9 @@ export const READ_PACKAGE_INFO_AUTHZ: FleetAuthzRouteConfig['fleetAuthz'] = { integrations: { readPackageInfo: true }, }; -export const registerRoutes = (router: FleetAuthzRouter) => { +export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => { + const experimentalFeatures = parseExperimentalConfigValue(config.enableExperimental); + router.versioned .get({ path: EPM_API_ROUTES.CATEGORIES_PATTERN, @@ -219,6 +230,38 @@ export const registerRoutes = (router: FleetAuthzRouter) => { installPackageFromRegistryHandler ); + if (experimentalFeatures.useSpaceAwareness) { + router.versioned + .post({ + path: EPM_API_ROUTES.INSTALL_KIBANA_ASSETS_PATTERN, + fleetAuthz: { + integrations: { installPackages: true }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { request: InstallKibanaAssetsRequestSchema }, + }, + installPackageKibanaAssetsHandler + ); + + router.versioned + .delete({ + path: EPM_API_ROUTES.DELETE_KIBANA_ASSETS_PATTERN, + fleetAuthz: { + integrations: { installPackages: true }, + }, + }) + .addVersion( + { + version: API_VERSIONS.public.v1, + validate: { request: DeleteKibanaAssetsRequestSchema }, + }, + deletePackageKibanaAssetsHandler + ); + } + router.versioned .post({ path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, diff --git a/x-pack/plugins/fleet/server/routes/epm/kibana_assets_handler.ts b/x-pack/plugins/fleet/server/routes/epm/kibana_assets_handler.ts new file mode 100644 index 0000000000000..8fe83f98669d1 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/epm/kibana_assets_handler.ts @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TypeOf } from '@kbn/config-schema'; + +import { defaultFleetErrorHandler, FleetNotFoundError } from '../../errors'; +import { appContextService } from '../../services'; +import { + deleteKibanaAssetsAndReferencesForSpace, + installKibanaAssetsAndReferences, +} from '../../services/epm/kibana/assets/install'; +import { + getInstallationObject, + getInstalledPackageWithAssets, +} from '../../services/epm/packages/get'; +import type { + DeleteKibanaAssetsRequestSchema, + FleetRequestHandler, + InstallKibanaAssetsRequestSchema, +} from '../../types'; + +export const installPackageKibanaAssetsHandler: FleetRequestHandler< + TypeOf, + undefined, + TypeOf +> = async (context, request, response) => { + try { + const fleetContext = await context.fleet; + const savedObjectsClient = fleetContext.internalSoClient; + const logger = appContextService.getLogger(); + const spaceId = fleetContext.spaceId; + const { pkgName, pkgVersion } = request.params; + + const installedPkgWithAssets = await getInstalledPackageWithAssets({ + savedObjectsClient, + pkgName, + logger, + }); + + const installation = await getInstallationObject({ + pkgName, + savedObjectsClient, + }); + + if ( + !installation || + !installedPkgWithAssets || + installedPkgWithAssets?.installation.version !== pkgVersion + ) { + throw new FleetNotFoundError('Requested version is not installed'); + } + + const { packageInfo } = installedPkgWithAssets; + + await installKibanaAssetsAndReferences({ + savedObjectsClient, + logger, + pkgName, + pkgTitle: packageInfo.title, + installAsAdditionalSpace: true, + spaceId, + assetTags: installedPkgWithAssets.packageInfo?.asset_tags, + installedPkg: installation, + packageInstallContext: { + packageInfo, + paths: installedPkgWithAssets.paths, + assetsMap: installedPkgWithAssets.assetsMap, + }, + }); + + return response.ok({ body: { success: true } }); + } catch (error) { + return await defaultFleetErrorHandler({ error, response }); + } +}; + +export const deletePackageKibanaAssetsHandler: FleetRequestHandler< + TypeOf, + undefined +> = async (context, request, response) => { + try { + const fleetContext = await context.fleet; + const savedObjectsClient = fleetContext.internalSoClient; + const logger = appContextService.getLogger(); + const spaceId = fleetContext.spaceId; + const { pkgName, pkgVersion } = request.params; + + const installation = await getInstallationObject({ + pkgName, + savedObjectsClient, + }); + + if (!installation || installation.attributes.version !== pkgVersion) { + throw new FleetNotFoundError('Version is not installed'); + } + + await deleteKibanaAssetsAndReferencesForSpace({ + savedObjectsClient, + logger, + pkgName, + spaceId, + installedPkg: installation, + }); + + return response.ok({ body: { success: true } }); + } catch (error) { + return await defaultFleetErrorHandler({ error, response }); + } +}; diff --git a/x-pack/plugins/fleet/server/routes/index.ts b/x-pack/plugins/fleet/server/routes/index.ts index 5177b85d84dea..39b3f0220d8ba 100644 --- a/x-pack/plugins/fleet/server/routes/index.ts +++ b/x-pack/plugins/fleet/server/routes/index.ts @@ -32,7 +32,7 @@ export function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: Fleet registerAppRoutes(fleetAuthzRouter); // The upload package route is only authorized for the superuser - registerEPMRoutes(fleetAuthzRouter); + registerEPMRoutes(fleetAuthzRouter, config); registerSetupRoutes(fleetAuthzRouter, config); registerAgentPolicyRoutes(fleetAuthzRouter); diff --git a/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts b/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts index 122a8d7a7ddc7..45b0995aac078 100644 --- a/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts +++ b/x-pack/plugins/fleet/server/routes/utils/filter_utils_real_queries.test.ts @@ -515,7 +515,7 @@ describe('validateKuery validates real kueries', () => { beforeEach(() => { mockedAppContextService.getExperimentalFeatures.mockReturnValue({ enableStrictKQLValidation: true, - }); + } as any); }); afterEach(() => { mockedAppContextService.getExperimentalFeatures.mockReset(); @@ -849,7 +849,7 @@ describe('validateKuery validates real kueries', () => { beforeEach(() => { mockedAppContextService.getExperimentalFeatures.mockReturnValue({ enableStrictKQLValidation: false, - }); + } as any); }); it('Allows to skip validation for a free text query', async () => { diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index f3377b6665cc0..b471491edc3f3 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -669,6 +669,10 @@ export const getSavedObjectTypes = ( dynamic: false, properties: {}, }, + additional_spaces_installed_kibana: { + type: 'flattened', + index: false, + }, install_started_at: { type: 'date' }, install_version: { type: 'keyword' }, install_status: { type: 'keyword' }, @@ -711,6 +715,16 @@ export const getSavedObjectTypes = ( }, ], }, + '3': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + additional_spaces_installed_kibana: { type: 'flattened', index: false }, + }, + }, + ], + }, }, migrations: { '7.14.0': migrateInstallationToV7140, diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts index 3547fc70daa15..3b200ac5115cf 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.test.ts @@ -10,10 +10,9 @@ import type { SavedObjectsImportSuccess, SavedObjectsImportResponse, } from '@kbn/core/server'; - import { loggingSystemMock } from '@kbn/core/server/mocks'; -import type { ArchiveAsset } from './install'; +import { type ArchiveAsset } from './install'; jest.mock('timers/promises', () => ({ async setTimeout() {}, diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 2956cb5fe20c2..e7e453648a596 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -19,32 +19,22 @@ import type { import { createListStream } from '@kbn/utils'; import { partition } from 'lodash'; -import type { IAssignmentService, ITagsClient } from '@kbn/saved-objects-tagging-plugin/server'; - -import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common'; import { getAssetFromAssetsMap, getPathParts } from '../../archive'; import { KibanaAssetType, KibanaSavedObjectType } from '../../../../types'; -import type { - AssetType, - AssetReference, - AssetParts, - Installation, - PackageSpecTags, -} from '../../../../types'; -import { savedObjectTypes } from '../../packages'; -import type { PackageInstallContext } from '../../../../../common/types'; +import type { AssetReference, AssetParts, Installation, PackageSpecTags } from '../../../../types'; +import type { KibanaAssetReference, PackageInstallContext } from '../../../../../common/types'; import { indexPatternTypes, getIndexPatternSavedObjects, makeManagedIndexPatternsGlobal, } from '../index_pattern/install'; -import { saveKibanaAssetsRefs } from '../../packages/install'; +import { kibanaAssetsToAssetsRef, saveKibanaAssetsRefs } from '../../packages/install'; import { deleteKibanaSavedObjectsAssets } from '../../packages/remove'; -import { KibanaSOReferenceError } from '../../../../errors'; - +import { FleetError, KibanaSOReferenceError } from '../../../../errors'; import { withPackageSpan } from '../../packages/utils'; import { tagKibanaAssets } from './tag_assets'; +import { getSpaceAwareSaveobjectsClients } from './saved_objects'; type SavedObjectsImporterContract = Pick; const formatImportErrorsForLog = (errors: SavedObjectsImportFailure[]) => @@ -163,11 +153,8 @@ export async function installKibanaAssets(options: { return installedAssets; } -export async function installKibanaAssetsAndReferences({ +export async function installKibanaAssetsAndReferencesMultispace({ savedObjectsClient, - savedObjectsImporter, - savedObjectTagAssignmentService, - savedObjectTagClient, logger, pkgName, pkgTitle, @@ -175,11 +162,9 @@ export async function installKibanaAssetsAndReferences({ installedPkg, spaceId, assetTags, + installAsAdditionalSpace, }: { savedObjectsClient: SavedObjectsClientContract; - savedObjectsImporter: Pick; - savedObjectTagAssignmentService: IAssignmentService; - savedObjectTagClient: ITagsClient; logger: Logger; pkgName: string; pkgTitle: string; @@ -187,15 +172,89 @@ export async function installKibanaAssetsAndReferences({ installedPkg?: SavedObject; spaceId: string; assetTags?: PackageSpecTags[]; + installAsAdditionalSpace?: boolean; }) { - const kibanaAssets = await getKibanaAssets(packageInstallContext); - if (installedPkg) await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg }); - // save new kibana refs before installing the assets - const installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + if (installedPkg && !installAsAdditionalSpace) { + // Install in every space => upgrades + const refs = await installKibanaAssetsAndReferences({ + savedObjectsClient, + logger, + pkgName, + pkgTitle, + packageInstallContext, + installedPkg, + spaceId, + assetTags, + installAsAdditionalSpace, + }); + + for (const additionnalSpaceId of Object.keys( + installedPkg.attributes.additional_spaces_installed_kibana ?? {} + )) { + await installKibanaAssetsAndReferences({ + savedObjectsClient, + logger, + pkgName, + pkgTitle, + packageInstallContext, + installedPkg, + spaceId: additionnalSpaceId, + assetTags, + installAsAdditionalSpace: true, + }); + } + return refs; + } + + return installKibanaAssetsAndReferences({ savedObjectsClient, + logger, pkgName, - kibanaAssets - ); + pkgTitle, + packageInstallContext, + installedPkg, + spaceId, + assetTags, + installAsAdditionalSpace, + }); +} + +export async function installKibanaAssetsAndReferences({ + savedObjectsClient, + logger, + pkgName, + pkgTitle, + packageInstallContext, + installedPkg, + spaceId, + assetTags, + installAsAdditionalSpace, +}: { + savedObjectsClient: SavedObjectsClientContract; + logger: Logger; + pkgName: string; + pkgTitle: string; + packageInstallContext: PackageInstallContext; + installedPkg?: SavedObject; + spaceId: string; + assetTags?: PackageSpecTags[]; + installAsAdditionalSpace?: boolean; +}) { + const { savedObjectsImporter, savedObjectTagAssignmentService, savedObjectTagClient } = + getSpaceAwareSaveobjectsClients(spaceId); + const kibanaAssets = await getKibanaAssets(packageInstallContext); + if (installedPkg) { + await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg, spaceId }); + } + let installedKibanaAssetsRefs: KibanaAssetReference[] = []; + if (!installAsAdditionalSpace) { + // save new kibana refs before installing the assets + installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + kibanaAssetsToAssetsRef(kibanaAssets) + ); + } const importedAssets = await installKibanaAssets({ savedObjectsClient, @@ -204,6 +263,24 @@ export async function installKibanaAssetsAndReferences({ pkgName, kibanaAssets, }); + if (installAsAdditionalSpace) { + const assets = importedAssets.map( + ({ id, type, destinationId }) => + ({ + id: destinationId ?? id, + originId: id, + type, + } as KibanaAssetReference) + ); + installedKibanaAssetsRefs = await saveKibanaAssetsRefs( + savedObjectsClient, + pkgName, + assets, + installedPkg && installedPkg.attributes.installed_kibana_space_id === spaceId + ? false + : installAsAdditionalSpace + ); + } await withPackageSpan('Create and assign package tags', () => tagKibanaAssets({ savedObjectTagAssignmentService, @@ -220,20 +297,32 @@ export async function installKibanaAssetsAndReferences({ return installedKibanaAssetsRefs; } -export const deleteKibanaInstalledRefs = async ( - savedObjectsClient: SavedObjectsClientContract, - pkgName: string, - installedKibanaRefs: AssetReference[] -) => { - const installedAssetsToSave = installedKibanaRefs.filter(({ id, type }) => { - const assetType = type as AssetType; - return !savedObjectTypes.includes(assetType); - }); +export async function deleteKibanaAssetsAndReferencesForSpace({ + savedObjectsClient, + logger, + pkgName, + installedPkg, + spaceId, +}: { + savedObjectsClient: SavedObjectsClientContract; + logger: Logger; + pkgName: string; + installedPkg: SavedObject; + spaceId: string; +}) { + if (!installedPkg) { + return; + } + + if (installedPkg.attributes.installed_kibana_space_id === spaceId) { + throw new FleetError( + 'Impossible to delete kibana assets from the space where the package was installed, you must uninstall the package.' + ); + } + await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg, spaceId }); + await saveKibanaAssetsRefs(savedObjectsClient, pkgName, [], true); +} - return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { - installed_kibana: installedAssetsToSave, - }); -}; export async function getKibanaAssets( packageInstallContext: PackageInstallContext ): Promise> { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/saved_object.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/saved_object.test.ts new file mode 100644 index 0000000000000..8ac5703265075 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/saved_object.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock, savedObjectsServiceMock } from '@kbn/core/server/mocks'; + +import { appContextService } from '../../../app_context'; + +import { getSpaceAwareSaveobjectsClients } from './saved_objects'; + +jest.mock('../../../app_context'); + +describe('getSpaceAwareSaveobjectsClients', () => { + it('return space scopped clients', () => { + const soStartMock = savedObjectsServiceMock.createStartContract(); + const mockedSavedObjectTagging = { + createInternalAssignmentService: jest.fn(), + createTagClient: jest.fn(), + }; + + const scoppedSoClient = savedObjectsClientMock.create(); + jest + .mocked(appContextService.getInternalUserSOClientForSpaceId) + .mockReturnValue(scoppedSoClient); + + jest.mocked(appContextService.getSavedObjects).mockReturnValue(soStartMock); + jest.mocked(appContextService.getSavedObjectsTagging).mockReturnValue(mockedSavedObjectTagging); + + getSpaceAwareSaveobjectsClients('test1'); + + expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledWith('test1'); + expect(soStartMock.createImporter).toBeCalledWith(scoppedSoClient, expect.anything()); + expect(mockedSavedObjectTagging.createInternalAssignmentService).toBeCalledWith({ + client: scoppedSoClient, + }); + expect(mockedSavedObjectTagging.createTagClient).toBeCalledWith({ client: scoppedSoClient }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/saved_objects.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/saved_objects.ts new file mode 100644 index 0000000000000..c4ff55ca8ac82 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/saved_objects.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { appContextService } from '../../../app_context'; + +export function getSpaceAwareSaveobjectsClients(spaceId?: string) { + // Saved object client need to be scopped with the package space for saved object tagging + const savedObjectClientWithSpace = appContextService.getInternalUserSOClientForSpaceId(spaceId); + + const savedObjectsImporter = appContextService + .getSavedObjects() + .createImporter(savedObjectClientWithSpace, { importSizeLimit: 15_000 }); + + const savedObjectTagAssignmentService = appContextService + .getSavedObjectsTagging() + .createInternalAssignmentService({ client: savedObjectClientWithSpace }); + + const savedObjectTagClient = appContextService + .getSavedObjectsTagging() + .createTagClient({ client: savedObjectClientWithSpace }); + + return { + savedObjectClientWithSpace, + savedObjectsImporter, + savedObjectTagAssignmentService, + savedObjectTagClient, + }; +} diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index 780f1ceb60566..97b0eeb823e02 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -10,12 +10,9 @@ import type { Logger, SavedObject, SavedObjectsClientContract, - ISavedObjectsImporter, } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import type { IAssignmentService, ITagsClient } from '@kbn/saved-objects-tagging-plugin/server'; - import type { HTTPAuthorizationHeader } from '../../../../common/http_authorization_header'; import type { PackageInstallContext } from '../../../../common/types'; import { getNormalizedDataStreams } from '../../../../common/services'; @@ -60,9 +57,6 @@ import { installIndexTemplatesAndPipelines } from './install_index_template_pipe // only the more explicit `installPackage*` functions should be used export async function _installPackage({ savedObjectsClient, - savedObjectsImporter, - savedObjectTagAssignmentService, - savedObjectTagClient, esClient, logger, installedPkg, @@ -77,9 +71,6 @@ export async function _installPackage({ skipDataStreamRollover, }: { savedObjectsClient: SavedObjectsClientContract; - savedObjectsImporter: Pick; - savedObjectTagAssignmentService: IAssignmentService; - savedObjectTagClient: ITagsClient; esClient: ElasticsearchClient; logger: Logger; installedPkg?: SavedObject; @@ -157,9 +148,6 @@ export async function _installPackage({ const kibanaAssetPromise = withPackageSpan('Install Kibana assets', () => installKibanaAssetsAndReferences({ savedObjectsClient, - savedObjectsImporter, - savedObjectTagAssignmentService, - savedObjectTagClient, pkgName, pkgTitle, packageInstallContext, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts index 14f3068795120..53112c5eea673 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -382,39 +382,6 @@ describe('install', () => { expect(response.status).toEqual('installed'); }); - - it('should use a scoped to package space soClient for tagging', async () => { - const mockedTaggingSo = savedObjectsClientMock.create(); - jest - .mocked(appContextService.getInternalUserSOClientForSpaceId) - .mockReturnValue(mockedTaggingSo); - jest - .mocked(getInstallationObject) - .mockResolvedValueOnce({ attributes: { version: '1.2.0' } } as any); - - jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); - await installPackage({ - spaceId: 'test', - installSource: 'registry', - pkgkey: 'apache-1.3.0', - savedObjectsClient: savedObjectsClientMock.create(), - esClient: {} as ElasticsearchClient, - }); - - expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledWith('test'); - expect(appContextService.getSavedObjectsTagging().createTagClient).toBeCalledWith( - expect.objectContaining({ - client: mockedTaggingSo, - }) - ); - expect( - appContextService.getSavedObjectsTagging().createInternalAssignmentService - ).toBeCalledWith( - expect.objectContaining({ - client: mockedTaggingSo, - }) - ); - }); }); describe('with enablePackagesStateMachine = true', () => { @@ -632,39 +599,6 @@ describe('install', () => { expect(response.status).toEqual('installed'); }); - - it('should use a scoped to package space soClient for tagging', async () => { - const mockedTaggingSo = savedObjectsClientMock.create(); - jest - .mocked(appContextService.getInternalUserSOClientForSpaceId) - .mockReturnValue(mockedTaggingSo); - jest - .mocked(getInstallationObject) - .mockResolvedValueOnce({ attributes: { version: '1.2.0', installed_kibana: [] } } as any); - - jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); - await installPackage({ - spaceId: 'test', - installSource: 'registry', - pkgkey: 'apache-1.3.0', - savedObjectsClient: savedObjectsClientMock.create(), - esClient: {} as ElasticsearchClient, - }); - - expect(appContextService.getInternalUserSOClientForSpaceId).toBeCalledWith('test'); - expect(appContextService.getSavedObjectsTagging().createTagClient).toBeCalledWith( - expect.objectContaining({ - client: mockedTaggingSo, - }) - ); - expect( - appContextService.getSavedObjectsTagging().createInternalAssignmentService - ).toBeCalledWith( - expect.objectContaining({ - client: mockedTaggingSo, - }) - ); - }); }); }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 9f200e97c3cfd..f27cb794475c9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import semverLt from 'semver/functions/lt'; import type Boom from '@hapi/boom'; import moment from 'moment'; +import { omit } from 'lodash'; import type { ElasticsearchClient, SavedObject, @@ -21,7 +22,11 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import pRetry from 'p-retry'; import type { LicenseType } from '@kbn/licensing-plugin/server'; -import type { PackageDataStreamTypes, PackageInstallContext } from '../../../../common/types'; +import type { + KibanaAssetReference, + PackageDataStreamTypes, + PackageInstallContext, +} from '../../../../common/types'; import type { HTTPAuthorizationHeader } from '../../../../common/http_authorization_header'; import { isPackagePrerelease, getNormalizedDataStreams } from '../../../../common/services'; import { FLEET_INSTALL_FORMAT_VERSION } from '../../../constants/fleet_es_assets'; @@ -619,27 +624,9 @@ async function installPackageCommon(options: { return { error: err, installType, installSource }; } - // Saved object client need to be scopped with the package space for saved object tagging - const savedObjectClientWithSpace = appContextService.getInternalUserSOClientForSpaceId(spaceId); - - const savedObjectsImporter = appContextService - .getSavedObjects() - .createImporter(savedObjectClientWithSpace, { importSizeLimit: 15_000 }); - - const savedObjectTagAssignmentService = appContextService - .getSavedObjectsTagging() - .createInternalAssignmentService({ client: savedObjectClientWithSpace }); - - const savedObjectTagClient = appContextService - .getSavedObjectsTagging() - .createTagClient({ client: savedObjectClientWithSpace }); - // try installing the package, if there was an error, call error handler and rethrow return await _installPackage({ savedObjectsClient, - savedObjectsImporter, - savedObjectTagAssignmentService, - savedObjectTagClient, esClient, logger, installedPkg, @@ -1294,10 +1281,17 @@ export async function createInstallation(options: { return created; } +export const kibanaAssetsToAssetsRef = ( + kibanaAssets: Record +): KibanaAssetReference[] => { + return Object.values(kibanaAssets).flat().map(toAssetReference); +}; + export const saveKibanaAssetsRefs = async ( savedObjectsClient: SavedObjectsClientContract, pkgName: string, - kibanaAssets: Record + assetRefs: KibanaAssetReference[], + saveAsAdditionnalSpace = false ) => { auditLoggingService.writeCustomSoAuditLog({ action: 'update', @@ -1305,20 +1299,43 @@ export const saveKibanaAssetsRefs = async ( savedObjectType: PACKAGES_SAVED_OBJECT_TYPE, }); - const assetRefs = Object.values(kibanaAssets).flat().map(toAssetReference); + const spaceId = savedObjectsClient.getCurrentNamespace() || DEFAULT_SPACE_ID; + // Because Kibana assets are installed in parallel with ES assets with refresh: false, we almost always run into an // issue that causes a conflict error due to this issue: https://github.com/elastic/kibana/issues/126240. This is safe // to retry constantly until it succeeds to optimize this critical user journey path as much as possible. await pRetry( - () => - savedObjectsClient.update( + async () => { + const installation = saveAsAdditionnalSpace + ? await savedObjectsClient + .get(PACKAGES_SAVED_OBJECT_TYPE, pkgName) + .catch((e) => { + if (SavedObjectsErrorHelpers.isNotFoundError(e)) { + return undefined; + } + throw e; + }) + : undefined; + + return savedObjectsClient.update( PACKAGES_SAVED_OBJECT_TYPE, pkgName, - { - installed_kibana: assetRefs, - }, + saveAsAdditionnalSpace + ? { + additional_spaces_installed_kibana: { + ...omit( + installation?.attributes?.additional_spaces_installed_kibana ?? {}, + spaceId + ), + ...(assetRefs.length > 0 ? { [spaceId]: assetRefs } : {}), + }, + } + : { + installed_kibana: assetRefs, + }, { refresh: false } - ), + ); + }, { retries: 20 } // Use a number of retries higher than the number of es asset update operations ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.test.ts index c77433774a5cf..5e4dd084b2274 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.test.ts @@ -41,7 +41,7 @@ jest.mock('../../elasticsearch/ilm/install'); jest.mock('../../elasticsearch/datastream_ilm/install'); import { updateCurrentWriteIndices } from '../../elasticsearch/template/template'; -import { installKibanaAssetsAndReferences } from '../../kibana/assets/install'; +import { installKibanaAssetsAndReferencesMultispace } from '../../kibana/assets/install'; import { MAX_TIME_COMPLETE_INSTALL } from '../../../../../common/constants'; @@ -58,7 +58,9 @@ const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.Mocked typeof updateCurrentWriteIndices >; const mockedInstallKibanaAssetsAndReferences = - installKibanaAssetsAndReferences as jest.MockedFunction; + installKibanaAssetsAndReferencesMultispace as jest.MockedFunction< + typeof installKibanaAssetsAndReferencesMultispace + >; function sleep(millis: number) { return new Promise((resolve) => setTimeout(resolve, millis)); @@ -293,9 +295,10 @@ describe('_stateMachineInstallPackage', () => { describe('When timeout is reached', () => { it('restarts installation', async () => { await _stateMachineInstallPackage({ + installSource: 'registry', + installType: 'install', + spaceId: 'default', savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), esClient, logger: loggerMock.create(), packageInstallContext: { @@ -326,9 +329,10 @@ describe('_stateMachineInstallPackage', () => { describe('With no force flag', () => { it('throws concurrent installation error', async () => { const installPromise = _stateMachineInstallPackage({ + installSource: 'registry', + installType: 'install', + spaceId: 'default', savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), esClient, logger: loggerMock.create(), packageInstallContext: { @@ -356,9 +360,10 @@ describe('_stateMachineInstallPackage', () => { describe('With force flag provided', () => { it('restarts installation', async () => { await _stateMachineInstallPackage({ + installSource: 'registry', + installType: 'install', + spaceId: 'default', savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), esClient, logger: loggerMock.create(), packageInstallContext: { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.ts index d66334b315a42..afad28d28a461 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/_state_machine_package_install.ts @@ -9,12 +9,9 @@ import type { Logger, SavedObject, SavedObjectsClientContract, - ISavedObjectsImporter, } from '@kbn/core/server'; import { SavedObjectsErrorHelpers } from '@kbn/core/server'; -import type { IAssignmentService, ITagsClient } from '@kbn/saved-objects-tagging-plugin/server'; - import { PackageSavedObjectConflictError } from '../../../../errors'; import type { HTTPAuthorizationHeader } from '../../../../../common/http_authorization_header'; @@ -53,9 +50,6 @@ import { handleState } from './state_machine'; export interface InstallContext extends StateContext { savedObjectsClient: SavedObjectsClientContract; - savedObjectsImporter: Pick; - savedObjectTagAssignmentService: IAssignmentService; - savedObjectTagClient: ITagsClient; esClient: ElasticsearchClient; logger: Logger; installedPkg?: SavedObject; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.test.ts index e13e3c9b095b2..7d466f03d52fe 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.test.ts @@ -12,14 +12,15 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants'; import { appContextService } from '../../../../app_context'; import { createAppContextStartContractMock } from '../../../../../mocks'; -import { installKibanaAssetsAndReferences } from '../../../kibana/assets/install'; +import { installKibanaAssetsAndReferencesMultispace } from '../../../kibana/assets/install'; jest.mock('../../../kibana/assets/install'); import { stepInstallKibanaAssets } from './step_install_kibana_assets'; -const mockedInstallKibanaAssetsAndReferences = - installKibanaAssetsAndReferences as jest.MockedFunction; +const mockedInstallKibanaAssetsAndReferencesMultispace = jest.mocked( + installKibanaAssetsAndReferencesMultispace +); describe('stepInstallKibanaAssets', () => { let soClient: jest.Mocked; @@ -42,8 +43,6 @@ describe('stepInstallKibanaAssets', () => { it('Should call installKibanaAssetsAndReferences', async () => { const installationPromise = stepInstallKibanaAssets({ savedObjectsClient: soClient, - // @ts-ignore - savedObjectsImporter: jest.fn(), esClient, logger: loggerMock.create(), packageInstallContext: { @@ -68,14 +67,14 @@ describe('stepInstallKibanaAssets', () => { }); await expect(installationPromise).resolves.not.toThrowError(); - expect(mockedInstallKibanaAssetsAndReferences).toBeCalledTimes(1); + expect(mockedInstallKibanaAssetsAndReferencesMultispace).toBeCalledTimes(1); }); esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; appContextService.start(createAppContextStartContractMock()); it('Should correctly handle errors', async () => { // force errors from this function - mockedInstallKibanaAssetsAndReferences.mockImplementation(async () => { + mockedInstallKibanaAssetsAndReferencesMultispace.mockImplementation(async () => { throw new Error('mocked async error A: should be caught'); }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.ts b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.ts index 56649c04428ac..2db6f622d3281 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install_state_machine/steps/step_install_kibana_assets.ts @@ -5,32 +5,20 @@ * 2.0. */ -import { installKibanaAssetsAndReferences } from '../../../kibana/assets/install'; +import { installKibanaAssetsAndReferencesMultispace } from '../../../kibana/assets/install'; import { withPackageSpan } from '../../utils'; import type { InstallContext } from '../_state_machine_package_install'; export async function stepInstallKibanaAssets(context: InstallContext) { - const { - savedObjectsClient, - savedObjectsImporter, - savedObjectTagAssignmentService, - savedObjectTagClient, - logger, - installedPkg, - packageInstallContext, - spaceId, - } = context; + const { savedObjectsClient, logger, installedPkg, packageInstallContext, spaceId } = context; const { packageInfo } = packageInstallContext; const { name: pkgName, title: pkgTitle } = packageInfo; const kibanaAssetPromise = withPackageSpan('Install Kibana assets', () => - installKibanaAssetsAndReferences({ + installKibanaAssetsAndReferencesMultispace({ savedObjectsClient, - savedObjectsImporter, - savedObjectTagAssignmentService, - savedObjectTagClient, pkgName, pkgTitle, packageInstallContext, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index 436c2efaa2275..5369da9da82e2 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -181,6 +181,7 @@ async function deleteAssets( installed_es: installedEs, installed_kibana: installedKibana, installed_kibana_space_id: spaceId = DEFAULT_SPACE_ID, + additional_spaces_installed_kibana: installedInAdditionalSpacesKibana = {}, }: Installation, savedObjectsClient: SavedObjectsClientContract, esClient: ElasticsearchClient @@ -237,6 +238,9 @@ async function deleteAssets( await Promise.all([ ...deleteESAssets(otherAssets, esClient), deleteKibanaAssets(installedKibana, spaceId), + Object.entries(installedInAdditionalSpacesKibana).map(([additionalSpaceId, kibanaAssets]) => + deleteKibanaAssets(kibanaAssets, additionalSpaceId) + ), ]); } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error @@ -271,21 +275,32 @@ async function deleteComponentTemplate(esClient: ElasticsearchClient, name: stri export async function deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg, + spaceId, }: { savedObjectsClient: SavedObjectsClientContract; installedPkg: SavedObject; + spaceId?: string; }) { - const { installed_kibana: installedRefs, installed_kibana_space_id: spaceId } = - installedPkg.attributes; - if (!installedRefs.length) return; + const { installed_kibana_space_id: installedSpaceId } = installedPkg.attributes; + + let refsToDelete: KibanaAssetReference[]; + let spaceIdToDelete: string | undefined; + if (!spaceId || spaceId === installedSpaceId) { + refsToDelete = installedPkg.attributes.installed_kibana; + spaceIdToDelete = installedSpaceId; + } else { + refsToDelete = installedPkg.attributes.additional_spaces_installed_kibana?.[spaceId] ?? []; + spaceIdToDelete = spaceId; + } + if (!refsToDelete.length) return; const logger = appContextService.getLogger(); - const assetsToDelete = installedRefs + const assetsToDelete = refsToDelete .filter(({ type }) => kibanaSavedObjectTypes.includes(type)) .map(({ id, type }) => ({ id, type } as KibanaAssetReference)); try { - await deleteKibanaAssets(assetsToDelete, spaceId); + await deleteKibanaAssets(assetsToDelete, spaceIdToDelete); } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error if (!SavedObjectsErrorHelpers.isNotFoundError(err)) { diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index 887a0ac9e0c8f..3bc9003162f44 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -40,7 +40,7 @@ mockedAppContextService.getLogger.mockImplementation(() => { } as unknown as Logger; }); -mockedAppContextService.getExperimentalFeatures.mockReturnValue({}); +mockedAppContextService.getExperimentalFeatures.mockReturnValue({} as any); const mockedAgentPolicyService = agentPolicyService as jest.Mocked; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index 3264926b39498..4b83c7f7c7c58 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -241,6 +241,26 @@ export const DeletePackageRequestSchema = { ), }; +export const InstallKibanaAssetsRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + pkgVersion: schema.string(), + }), + // body is deprecated on delete request + body: schema.nullable( + schema.object({ + force: schema.maybe(schema.boolean()), + }) + ), +}; + +export const DeleteKibanaAssetsRequestSchema = { + params: schema.object({ + pkgName: schema.string(), + pkgVersion: schema.string(), + }), +}; + export const DeletePackageRequestSchemaDeprecated = { params: schema.object({ pkgkey: schema.string(), diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts index 4b166d040625b..c14f87447e154 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/api_helper.ts @@ -20,6 +20,7 @@ import { PostEnrollmentAPIKeyResponse, PostEnrollmentAPIKeyRequest, GetEnrollmentSettingsResponse, + GetInfoResponse, } from '@kbn/fleet-plugin/common/types'; import { GetUninstallTokenResponse, @@ -173,4 +174,53 @@ export class SpaceTestApiClient { return res; } + // Package install + async getPackage( + { pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }, + spaceId?: string + ): Promise { + const { body: res } = await this.supertest + .get(`${this.getBaseUrl(spaceId)}/api/fleet/epm/packages/${pkgName}/${pkgVersion}`) + .expect(200); + + return res; + } + async installPackage( + { pkgName, pkgVersion, force }: { pkgName: string; pkgVersion: string; force?: boolean }, + spaceId?: string + ) { + const { body: res } = await this.supertest + .post(`${this.getBaseUrl(spaceId)}/api/fleet/epm/packages/${pkgName}/${pkgVersion}`) + .set('kbn-xsrf', 'xxxx') + .send({ force }) + .expect(200); + + return res; + } + async deletePackageKibanaAssets( + { pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }, + spaceId?: string + ) { + const { body: res } = await this.supertest + .delete( + `${this.getBaseUrl(spaceId)}/api/fleet/epm/packages/${pkgName}/${pkgVersion}/kibana_assets` + ) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + return res; + } + async installPackageKibanaAssets( + { pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }, + spaceId?: string + ) { + const { body: res } = await this.supertest + .post( + `${this.getBaseUrl(spaceId)}/api/fleet/epm/packages/${pkgName}/${pkgVersion}/kibana_assets` + ) + .set('kbn-xsrf', 'xxxx') + .expect(200); + + return res; + } } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/index.js b/x-pack/test/fleet_api_integration/apis/space_awareness/index.js index 3a3d9ea907150..9733668cd913d 100644 --- a/x-pack/test/fleet_api_integration/apis/space_awareness/index.js +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/index.js @@ -12,5 +12,6 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./agent_policies')); loadTestFile(require.resolve('./agents')); loadTestFile(require.resolve('./enrollment_settings')); + loadTestFile(require.resolve('./package_install')); }); } diff --git a/x-pack/test/fleet_api_integration/apis/space_awareness/package_install.ts b/x-pack/test/fleet_api_integration/apis/space_awareness/package_install.ts new file mode 100644 index 0000000000000..ed464bd8d9f31 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/space_awareness/package_install.ts @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { SpaceTestApiClient } from './api_helper'; +import { cleanFleetIndices } from './helpers'; +import { setupTestSpaces, TEST_SPACE_1 } from './space_helpers'; + +export default function (providerContext: FtrProviderContext) { + const { getService } = providerContext; + const supertest = getService('supertest'); + const esClient = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('package install', async function () { + skipIfNoDockerRegistry(providerContext); + const apiClient = new SpaceTestApiClient(supertest); + + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + }); + + setupTestSpaces(providerContext); + + describe('kibana_assets', () => { + describe('with package installed in default space', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + await apiClient.installPackage({ + pkgName: 'nginx', + pkgVersion: '1.20.0', + force: true, // To avoid package verification + }); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + }); + + it('should not allow to install kibana assets for a non installed package', async () => { + let err: Error | undefined; + try { + await apiClient.installPackageKibanaAssets({ pkgName: 'test', pkgVersion: '1.0.0' }); + } catch (_err) { + err = _err; + } + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should not allow to install kibana assets for a non installed package version', async () => { + let err: Error | undefined; + try { + await apiClient.installPackageKibanaAssets({ pkgName: 'nginx', pkgVersion: '1.19.0' }); + } catch (_err) { + err = _err; + } + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should allow to install kibana assets in default space', async () => { + await apiClient.installPackageKibanaAssets({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + + const res = await apiClient.getPackage({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + if (!('installationInfo' in res.item)) { + throw new Error('not installed'); + } + + expect(res.item.installationInfo?.installed_kibana_space_id).eql('default'); + expect(res.item.installationInfo?.additional_spaces_installed_kibana).eql(undefined); + }); + + it('should allow to install kibana assets in another space', async () => { + await apiClient.installPackageKibanaAssets( + { pkgName: 'nginx', pkgVersion: '1.20.0' }, + TEST_SPACE_1 + ); + + const res = await apiClient.getPackage({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + if (!('installationInfo' in res.item)) { + throw new Error('not installed'); + } + + expect(res.item.installationInfo?.installed_kibana_space_id).eql('default'); + expect( + Object.keys(res.item.installationInfo?.additional_spaces_installed_kibana ?? {}) + ).eql([TEST_SPACE_1]); + + const dashboard = res.item.installationInfo!.additional_spaces_installed_kibana?.[ + TEST_SPACE_1 + ]!.find((asset) => asset.originId === 'nginx-046212a0-a2a1-11e7-928f-5dbe6f6f5519'); + expect(dashboard).not.eql(undefined); + }); + + it('should not allow to delete kibana assets from default space', async () => { + let err: Error | undefined; + try { + await apiClient.deletePackageKibanaAssets({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + } catch (_err) { + err = _err; + } + expect(err).to.be.an(Error); + expect(err?.message).to.match(/400 "Bad Request"/); + }); + + it('should allow to delete kibana assets from test space', async () => { + await apiClient.deletePackageKibanaAssets( + { pkgName: 'nginx', pkgVersion: '1.20.0' }, + TEST_SPACE_1 + ); + + const res = await apiClient.getPackage({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + if (!('installationInfo' in res.item)) { + throw new Error('not installed'); + } + expect( + Object.keys(res.item.installationInfo?.additional_spaces_installed_kibana ?? {}) + ).eql([]); + }); + }); + + describe('with package installed in test space', () => { + before(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + await apiClient.installPackage( + { + pkgName: 'nginx', + pkgVersion: '1.20.0', + force: true, // To avoid package verification + }, + TEST_SPACE_1 + ); + }); + + after(async () => { + await kibanaServer.savedObjects.cleanStandardList(); + await kibanaServer.savedObjects.cleanStandardList({ + space: TEST_SPACE_1, + }); + await cleanFleetIndices(esClient); + }); + + it('should not allow to install kibana assets for a non installed package', async () => { + let err: Error | undefined; + try { + await apiClient.installPackageKibanaAssets({ pkgName: 'test', pkgVersion: '1.0.0' }); + } catch (_err) { + err = _err; + } + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should not allow to install kibana assets for a non installed package version', async () => { + let err: Error | undefined; + try { + await apiClient.installPackageKibanaAssets({ pkgName: 'nginx', pkgVersion: '1.19.0' }); + } catch (_err) { + err = _err; + } + expect(err).to.be.an(Error); + expect(err?.message).to.match(/404 "Not Found"/); + }); + + it('should allow to install kibana assets in test space', async () => { + await apiClient.installPackageKibanaAssets( + { pkgName: 'nginx', pkgVersion: '1.20.0' }, + TEST_SPACE_1 + ); + + const res = await apiClient.getPackage({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + if (!('installationInfo' in res.item)) { + throw new Error('not installed'); + } + + expect(res.item.installationInfo?.installed_kibana_space_id).eql(TEST_SPACE_1); + expect(res.item.installationInfo?.additional_spaces_installed_kibana).eql(undefined); + }); + + it('should allow to install kibana assets in default space', async () => { + await apiClient.installPackageKibanaAssets({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + + const res = await apiClient.getPackage({ pkgName: 'nginx', pkgVersion: '1.20.0' }); + if (!('installationInfo' in res.item)) { + throw new Error('not installed'); + } + + expect(res.item.installationInfo?.installed_kibana_space_id).eql(TEST_SPACE_1); + expect( + Object.keys(res.item.installationInfo?.additional_spaces_installed_kibana ?? {}) + ).eql(['default']); + + const dashboard = + res.item.installationInfo!.additional_spaces_installed_kibana?.default!.find( + (asset) => asset.originId === 'nginx-046212a0-a2a1-11e7-928f-5dbe6f6f5519' + ); + expect(dashboard).not.eql(undefined); + }); + }); + }); + }); +}