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);
+ });
+ });
+ });
+ });
+}