Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Allow to install Kibana assets in multiple spaces #186620

Merged
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@
"apiKey"
],
"epm-packages": [
"additional_spaces_installed_kibana",
"es_index_patterns",
"experimental_data_stream_features",
"experimental_data_stream_features.data_stream",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,10 @@
},
"epm-packages": {
"properties": {
"additional_spaces_installed_kibana": {
"index": false,
"type": "flattened"
},
"es_index_patterns": {
"dynamic": false,
"properties": {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/constants/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
25 changes: 15 additions & 10 deletions x-pack/plugins/fleet/common/experimental_features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, boolean>>({
const _allowedExperimentalValues = {
createPackagePolicyMultiPageLayout: true,
packageVerification: true,
showDevtoolsRequest: true,
Expand All @@ -32,9 +28,18 @@ export const allowedExperimentalValues = Object.freeze<Record<string, boolean>>(
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<
Copy link
Member Author

@nchaulet nchaulet Jun 21, 2024

Choose a reason for hiding this comment

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

Not really related to that feature, but it allow to add some type safety to allowedExperimentalValues , and fix the autocompletion that was bothering me

Record<keyof typeof _allowedExperimentalValues, boolean>
>({ ..._allowedExperimentalValues });

type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
type ExperimentalConfigKey = keyof ExperimentalFeatures;
type ExperimentalConfigKeys = ExperimentalConfigKey[];
type Mutable<T> = { -readonly [P in keyof T]: T[P] };

const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<ExperimentalConfigKeys>;
Expand All @@ -46,7 +51,7 @@ const allowedKeys = Object.keys(allowedExperimentalValues) as Readonly<Experimen
* @param configValue
*/
export const parseExperimentalConfigValue = (configValue: string[]): ExperimentalFeatures => {
const enabledFeatures: Mutable<ExperimentalFeatures> = {};
const enabledFeatures: Mutable<ExperimentalFeatures> = { ...allowedExperimentalValues };

for (const value of configValue) {
if (isValidExperimentalValue(value)) {
Expand All @@ -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];
6 changes: 6 additions & 0 deletions x-pack/plugins/fleet/common/services/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,7 @@ export interface StateContext<T> {

export interface Installation {
installed_kibana: KibanaAssetReference[];
additional_spaces_installed_kibana?: Record<string, KibanaAssetReference[]>;
installed_es: EsAssetReference[];
package_assets?: PackageAssetReference[];
es_index_patterns: Record<string, string>;
Expand Down Expand Up @@ -649,6 +650,7 @@ export type AssetReference = KibanaAssetReference | EsAssetReference;

export interface KibanaAssetReference {
id: string;
originId?: string;
type: KibanaSavedObjectType;
}
export interface EsAssetReference {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
);
Expand All @@ -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<any>).mockReturnValue({
...useStartServices(),
cloud: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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<string, Record<string, SimpleSOAssetType & { appLink?: string }>>
>({});
const [deferredInstallations, setDeferredInstallations] = useState<EsAssetReference[]>();

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) => {
Expand Down Expand Up @@ -231,6 +241,13 @@ export const AssetsPage = ({ packageInfo, refetchPackageInfo }: AssetsPanelProps
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

is there already a docs issue to update https://www.elastic.co/guide/en/fleet/current/install-uninstall-integration-assets.html once this feature is enabled by default? if not, let's go ahead and create a placeholder issue

Copy link
Member Author

Choose a reason for hiding this comment

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

No there is not, I will create one for the whole space awareness project as there is probably more than this to document.

/>
</p>
{useSpaceAwareness ? (
<InstallKibanaAssetsButton
installInfo={pkgInstallationInfo}
title={packageInfo.title}
onSuccess={forceRefreshAssets}
/>
) : null}
</EuiCallOut>

<EuiSpacer size="m" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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',
nchaulet marked this conversation as resolved.
Show resolved Hide resolved
}),
});
}
}, [mutateAsync, onSuccess, name, version, notifications.toasts]);

return (
<EuiButton
disabled={!canInstallPackages}
iconType="importAction"
isLoading={isLoading}
onClick={handleClickInstall}
>
{isLoading ? (
<FormattedMessage
id="xpack.fleet.integrations.installPackage.kibanaAssetsInstallingButtonLabel"
defaultMessage="Installing {title} kibana assets in current space"
values={{
title,
}}
/>
) : (
<FormattedMessage
id="xpack.fleet.integrations.installPackage.kibanaAssetsInstallButtonLabel"
defaultMessage="Install {title} kibana assets in current space"
nchaulet marked this conversation as resolved.
Show resolved Hide resolved
values={{
title,
}}
/>
)}
</EuiButton>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading