[Fleet] Ensure kibana assets are installed on policy space change (#215793)

This commit is contained in:
Nicolas Chaulet 2025-03-25 10:18:32 -04:00 committed by GitHub
parent 115ec32eec
commit bb78f8f5b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 224 additions and 76 deletions

View file

@ -27360,6 +27360,14 @@
"properties": {
"force": {
"type": "boolean"
},
"space_ids": {
"description": "When provided install assets in the specified spaces instead of the current space.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"type": "object"

View file

@ -27360,6 +27360,14 @@
"properties": {
"force": {
"type": "boolean"
},
"space_ids": {
"description": "When provided install assets in the specified spaces instead of the current space.",
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"type": "object"

View file

@ -25309,6 +25309,12 @@ paths:
properties:
force:
type: boolean
space_ids:
description: When provided install assets in the specified spaces instead of the current space.
items:
type: string
minItems: 1
type: array
responses:
'200':
content:

View file

@ -27513,6 +27513,12 @@ paths:
properties:
force:
type: boolean
space_ids:
description: When provided install assets in the specified spaces instead of the current space.
items:
type: string
minItems: 1
type: array
responses:
'200':
content:

View file

@ -34,7 +34,7 @@ import { validatePackagePolicy, validationHasErrors } from '../../../services';
import { NotObscuredByBottomBar } from '..';
import { StepConfigurePackagePolicy, StepDefinePackagePolicy } from '../../../components';
import { prepareInputPackagePolicyDataset } from '../../../services/prepare_input_pkg_policy_dataset';
import { ensurePackageKibanaAssetsInstalled } from '../../../services/ensure_kibana_assets_installed';
import { ensurePackageKibanaAssetsInstalled } from '../../../../../../services/ensure_kibana_assets_installed';
const ExpandableAdvancedSettings: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const [isShowingAdvanced, setIsShowingAdvanced] = useState<boolean>(false);

View file

@ -51,7 +51,7 @@ import {
getCloudShellUrlFromPackagePolicy,
} from '../../../../../../../components/cloud_security_posture/services';
import { AGENTLESS_DISABLED_INPUTS } from '../../../../../../../../common/constants';
import { ensurePackageKibanaAssetsInstalled } from '../../services/ensure_kibana_assets_installed';
import { ensurePackageKibanaAssetsInstalled } from '../../../../../services/ensure_kibana_assets_installed';
import { useAgentless, useSetupTechnology } from './setup_technology';

View file

@ -6,8 +6,9 @@
*/
import React, { memo, useMemo, useState } from 'react';
import deepEqual from 'fast-deep-equal';
import styled from 'styled-components';
import { pick } from 'lodash';
import { pick, uniqBy } from 'lodash';
import {
EuiBottomBar,
EuiFlexGroup,
@ -21,12 +22,14 @@ import { FormattedMessage } from '@kbn/i18n-react';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import { useHistory } from 'react-router-dom';
import { ensurePackageKibanaAssetsInstalled } from '../../../../../services/ensure_kibana_assets_installed';
import { useSpaceSettingsContext } from '../../../../../../../hooks/use_space_settings_context';
import type { AgentPolicy } from '../../../../../types';
import {
useStartServices,
useAuthz,
sendUpdateAgentPolicy,
sendUpdateAgentPolicyForRq,
useConfig,
sendGetAgentStatus,
useAgentPolicyRefresh,
@ -115,41 +118,54 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
const submitUpdateAgentPolicy = async () => {
setIsLoading(true);
try {
const { data, error } = await sendUpdateAgentPolicy(
agentPolicy.id,
pickAgentPolicyKeysToSend(agentPolicy)
);
if (data) {
notifications.toasts.addSuccess(
i18n.translate('xpack.fleet.editAgentPolicy.successNotificationTitle', {
defaultMessage: "Successfully updated ''{name}'' settings",
values: { name: agentPolicy.name },
})
const dataToSend = pickAgentPolicyKeysToSend(agentPolicy);
await sendUpdateAgentPolicyForRq(agentPolicy.id, pickAgentPolicyKeysToSend(agentPolicy));
if (
dataToSend.space_ids &&
!deepEqual(originalAgentPolicy.space_ids, dataToSend.space_ids)
) {
const packages = uniqBy(
originalAgentPolicy.package_policies
?.map((pp) =>
pp.package
? { pkgName: pp.package.name, pkgVersion: pp.package.version }
: undefined
)
.filter(
(p): p is { pkgName: string; pkgVersion: string } => typeof p !== 'undefined'
) ?? [],
'pkgName'
);
if (
agentPolicy.space_ids &&
!agentPolicy.space_ids.includes(spaceId ?? DEFAULT_SPACE_ID)
) {
history.replace(getPath('policies_list'));
} else {
refreshAgentPolicy();
setHasChanges(false);
for (const { pkgName, pkgVersion } of packages) {
await ensurePackageKibanaAssetsInstalled({
spaceIds: dataToSend.space_ids,
pkgName,
pkgVersion,
toasts: notifications.toasts,
});
}
} else {
notifications.toasts.addDanger(
error
? error.message
: i18n.translate('xpack.fleet.editAgentPolicy.errorNotificationTitle', {
defaultMessage: 'Unable to update agent policy',
})
);
}
} catch (e) {
notifications.toasts.addDanger(
i18n.translate('xpack.fleet.editAgentPolicy.errorNotificationTitle', {
defaultMessage: 'Unable to update agent policy',
notifications.toasts.addSuccess(
i18n.translate('xpack.fleet.editAgentPolicy.successNotificationTitle', {
defaultMessage: "Successfully updated ''{name}'' settings",
values: { name: agentPolicy.name },
})
);
if (agentPolicy.space_ids && !agentPolicy.space_ids.includes(spaceId ?? DEFAULT_SPACE_ID)) {
history.replace(getPath('policies_list'));
} else {
refreshAgentPolicy();
setHasChanges(false);
}
} catch (error) {
notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.editAgentPolicy.errorNotificationTitle', {
defaultMessage: 'Unable to update agent policy',
}),
});
}
setIsLoading(false);
};

View file

@ -7,14 +7,11 @@
import { toastsServiceMock } from '@kbn/core-notifications-browser-mocks/src/toasts_service.mock';
import {
sendGetPackageInfoByKeyForRq,
sendInstallKibanaAssetsForRq,
} from '../../../../../../hooks';
import { sendGetPackageInfoByKeyForRq, sendInstallKibanaAssetsForRq } from '../../../hooks';
import { ensurePackageKibanaAssetsInstalled } from './ensure_kibana_assets_installed';
jest.mock('../../../../../../hooks');
jest.mock('../../../hooks');
describe('ensurePackageKibanaAssetsInstalled', () => {
beforeEach(() => {
@ -48,6 +45,35 @@ describe('ensurePackageKibanaAssetsInstalled', () => {
expect(toasts.addSuccess).toBeCalled();
});
it('install assets in multiple space if not installed', async () => {
jest.mocked(sendGetPackageInfoByKeyForRq).mockResolvedValue({
item: {
installationInfo: {
name: 'nginx',
version: '1.25.1',
installed_kibana_space_id: 'default',
},
},
} as any);
const toasts = toastsServiceMock.createStartContract();
await ensurePackageKibanaAssetsInstalled({
spaceIds: ['default', 'test1', 'test2'],
pkgName: 'nginx',
pkgVersion: '1.25.1',
toasts,
});
expect(sendInstallKibanaAssetsForRq).toBeCalledWith({
pkgName: 'nginx',
pkgVersion: '1.25.1',
spaceIds: ['test1', 'test2'],
});
expect(toasts.addSuccess).toBeCalled();
});
it('does nothing if assets are already installed', async () => {
jest.mocked(sendGetPackageInfoByKeyForRq).mockResolvedValue({
item: {

View file

@ -9,22 +9,18 @@ import type { IToasts } from '@kbn/core/public';
import { i18n } from '@kbn/i18n';
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
import {
sendGetPackageInfoByKeyForRq,
sendInstallKibanaAssetsForRq,
} from '../../../../../../hooks';
import { sendGetPackageInfoByKeyForRq, sendInstallKibanaAssetsForRq } from '../../../hooks';
export async function ensurePackageKibanaAssetsInstalled({
currentSpaceId,
pkgName,
pkgVersion,
toasts,
...rest
}: {
currentSpaceId: string;
pkgName: string;
pkgVersion: string;
toasts: IToasts;
}) {
} & ({ currentSpaceId: string } | { spaceIds: string[] })) {
try {
const packageInfo = await sendGetPackageInfoByKeyForRq(pkgName, pkgVersion, {
prerelease: true,
@ -40,21 +36,43 @@ export async function ensurePackageKibanaAssetsInstalled({
installationInfo.installed_kibana_space_id ?? DEFAULT_SPACE_ID,
...Object.keys(installationInfo.additional_spaces_installed_kibana ?? {}),
];
if (!kibanaAssetsSpaces.includes(currentSpaceId)) {
if ('currentSpaceId' in rest) {
if (kibanaAssetsSpaces.includes(rest.currentSpaceId)) {
return;
}
await sendInstallKibanaAssetsForRq({
pkgName: installationInfo.name,
pkgVersion: installationInfo.version,
});
toasts.addSuccess(
i18n.translate('xpack.fleet.installKibanaAssets.successNotificationTitle', {
defaultMessage: 'Successfully installed kibana assets',
})
} else {
const missingSpaceIds = rest.spaceIds.filter(
(spaceId) => !kibanaAssetsSpaces.includes(spaceId)
);
if (!missingSpaceIds.length) {
return;
}
await sendInstallKibanaAssetsForRq({
pkgName: installationInfo.name,
pkgVersion: installationInfo.version,
spaceIds: missingSpaceIds,
});
}
toasts.addSuccess(
i18n.translate('xpack.fleet.installKibanaAssets.successNotificationTitle', {
defaultMessage: 'Successfully installed kibana assets for {pkgName}',
values: {
pkgName: installationInfo.name,
},
})
);
} catch (err) {
toasts.addError(err, {
title: i18n.translate('xpack.fleet.installKibanaAssets.errorNotificationTitle', {
defaultMessage: 'Unable to install kibana assets',
defaultMessage: 'Unable to install kibana assets for {pkgName}',
values: {
pkgName,
},
}),
});
}

View file

@ -184,6 +184,9 @@ export const sendCreateAgentPolicy = (
});
};
/**
* @deprecated use sendUpdateAgentPolicyForRq instead
*/
export const sendUpdateAgentPolicy = (
agentPolicyId: string,
body: UpdateAgentPolicyRequest['body']

View file

@ -339,6 +339,7 @@ interface UpdatePackageArgs {
interface InstallKibanaAssetsArgs {
pkgName: string;
pkgVersion: string;
spaceIds?: string[];
}
export const useUpdatePackageMutation = () => {
@ -365,11 +366,16 @@ export const useInstallKibanaAssetsMutation = () => {
});
};
export const sendInstallKibanaAssetsForRq = ({ pkgName, pkgVersion }: InstallKibanaAssetsArgs) =>
export const sendInstallKibanaAssetsForRq = ({
pkgName,
pkgVersion,
spaceIds,
}: InstallKibanaAssetsArgs) =>
sendRequestForRq({
path: epmRouteService.getInstallKibanaAssetsPath(pkgName, pkgVersion),
method: 'post',
version: API_VERSIONS.public.v1,
body: spaceIds ? { space_ids: spaceIds } : undefined,
});
export const sendUpdatePackage = (

View file

@ -5,9 +5,10 @@
* 2.0.
*/
import type { KibanaRequest } from '@kbn/core/server';
import type { TypeOf } from '@kbn/config-schema';
import { FleetNotFoundError } from '../../errors';
import { FleetError, FleetNotFoundError } from '../../errors';
import { appContextService } from '../../services';
import {
deleteKibanaAssetsAndReferencesForSpace,
@ -24,6 +25,21 @@ import type {
} from '../../types';
import { createArchiveIteratorFromMap } from '../../services/epm/archive/archive_iterator';
export async function checkIntegrationsAllPrivilegesForSpaces(
request: KibanaRequest,
spaceIds: string[]
) {
const security = appContextService.getSecurity();
const res = await security.authz.checkPrivilegesWithRequest(request).atSpaces(spaceIds, {
kibana: [security.authz.actions.api.get(`integrations-all`)],
});
if (!res.hasAllRequested) {
throw new FleetError(
`No enough permissions to install assets in spaces ${spaceIds.join(', ')}`
);
}
}
export const installPackageKibanaAssetsHandler: FleetRequestHandler<
TypeOf<typeof InstallKibanaAssetsRequestSchema.params>,
undefined,
@ -35,6 +51,10 @@ export const installPackageKibanaAssetsHandler: FleetRequestHandler<
const spaceId = fleetContext.spaceId;
const { pkgName, pkgVersion } = request.params;
if (request.body?.space_ids) {
await checkIntegrationsAllPrivilegesForSpaces(request, request.body?.space_ids);
}
const installedPkgWithAssets = await getInstalledPackageWithAssets({
savedObjectsClient,
pkgName,
@ -56,21 +76,25 @@ export const installPackageKibanaAssetsHandler: FleetRequestHandler<
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,
archiveIterator: createArchiveIteratorFromMap(installedPkgWithAssets.assetsMap),
},
});
const spaceIds = request.body?.space_ids ?? [spaceId];
for (const spaceToInstallId of spaceIds) {
await installKibanaAssetsAndReferences({
savedObjectsClient: appContextService.getInternalUserSOClientForSpaceId(spaceToInstallId),
logger,
pkgName,
pkgTitle: packageInfo.title,
installAsAdditionalSpace: true,
spaceId: spaceToInstallId,
assetTags: installedPkgWithAssets.packageInfo?.asset_tags,
installedPkg: installation,
packageInstallContext: {
packageInfo,
paths: installedPkgWithAssets.paths,
archiveIterator: createArchiveIteratorFromMap(installedPkgWithAssets.assetsMap),
},
});
}
return response.ok({ body: { success: true } });
};

View file

@ -345,7 +345,7 @@ export async function deleteKibanaAssetsAndReferencesForSpace({
);
}
await deleteKibanaSavedObjectsAssets({ installedPkg, spaceId });
await saveKibanaAssetsRefs(savedObjectsClient, pkgName, [], true);
await saveKibanaAssetsRefs(savedObjectsClient, pkgName, null, true);
}
const kibanaAssetTypes = Object.values(KibanaAssetType);

View file

@ -1199,7 +1199,7 @@ export const kibanaAssetsToAssetsRef = (
export const saveKibanaAssetsRefs = async (
savedObjectsClient: SavedObjectsClientContract,
pkgName: string,
assetRefs: KibanaAssetReference[],
assetRefs: KibanaAssetReference[] | null,
saveAsAdditionnalSpace = false
) => {
auditLoggingService.writeCustomSoAuditLog({
@ -1236,11 +1236,11 @@ export const saveKibanaAssetsRefs = async (
installation?.attributes?.additional_spaces_installed_kibana ?? {},
spaceId
),
...(assetRefs.length > 0 ? { [spaceId]: assetRefs } : {}),
...(assetRefs !== null ? { [spaceId]: assetRefs } : {}),
},
}
: {
installed_kibana: assetRefs,
installed_kibana: assetRefs !== null ? assetRefs : [],
},
{ refresh: false }
);
@ -1248,7 +1248,7 @@ export const saveKibanaAssetsRefs = async (
{ retries: 20 } // Use a number of retries higher than the number of es asset update operations
);
return assetRefs;
return assetRefs !== null ? assetRefs : [];
};
export async function ensurePackagesCompletedInstall(

View file

@ -591,10 +591,18 @@ export const InstallKibanaAssetsRequestSchema = {
pkgName: schema.string(),
pkgVersion: schema.string(),
}),
// body is deprecated on delete request
body: schema.nullable(
schema.object({
force: schema.maybe(schema.boolean()),
space_ids: schema.maybe(
schema.arrayOf(schema.string(), {
minSize: 1,
meta: {
description:
'When provided install assets in the specified spaces instead of the current space.',
},
})
),
})
),
};

View file

@ -482,7 +482,7 @@ export class SpaceTestApiClient {
return res;
}
async installPackageKibanaAssets(
{ pkgName, pkgVersion }: { pkgName: string; pkgVersion: string },
{ pkgName, pkgVersion, spaceIds }: { pkgName: string; pkgVersion: string; spaceIds?: string[] },
spaceId?: string
) {
const { body: res } = await this.supertest
@ -490,6 +490,7 @@ export class SpaceTestApiClient {
`${this.getBaseUrl(spaceId)}/api/fleet/epm/packages/${pkgName}/${pkgVersion}/kibana_assets`
)
.set('kbn-xsrf', 'xxxx')
.send(spaceIds ? { space_ids: spaceIds } : {})
.expect(200);
return res;

View file

@ -145,6 +145,24 @@ export default function (providerContext: FtrProviderContext) {
Object.keys(res.item.installationInfo?.additional_spaces_installed_kibana ?? {})
).eql([]);
});
it('should allow to install kibana in another space from the default space', async () => {
await apiClient.installPackageKibanaAssets({
pkgName: 'nginx',
pkgVersion: '1.20.0',
spaceIds: [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]);
});
});
describe('with package installed in test space', () => {