mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
[Fleet] Allow to bulk upgrade integrations (#215419)
This commit is contained in:
parent
b8a29d4096
commit
2aa857643d
25 changed files with 1363 additions and 113 deletions
|
@ -22,12 +22,13 @@ export const LIMITED_CONCURRENCY_ROUTE_TAG = 'ingest:limited-concurrency';
|
|||
// EPM API routes
|
||||
const EPM_PACKAGES_MANY = `${EPM_API_ROOT}/packages`;
|
||||
const EPM_PACKAGES_INSTALLED = `${EPM_API_ROOT}/packages/installed`;
|
||||
const EPM_PACKAGES_BULK = `${EPM_PACKAGES_MANY}/_bulk`;
|
||||
const EPM_PACKAGES_ONE_WITHOUT_VERSION = `${EPM_PACKAGES_MANY}/{pkgName}`;
|
||||
const EPM_PACKAGES_ONE = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion}`;
|
||||
const EPM_PACKAGES_ONE_WITH_OPTIONAL_VERSION = `${EPM_PACKAGES_MANY}/{pkgName}/{pkgVersion?}`;
|
||||
export const EPM_API_ROUTES = {
|
||||
BULK_INSTALL_PATTERN: EPM_PACKAGES_BULK,
|
||||
BULK_INSTALL_PATTERN: `${EPM_PACKAGES_MANY}/_bulk`,
|
||||
BULK_UPGRADE_PATTERN: `${EPM_PACKAGES_MANY}/_bulk_upgrade`,
|
||||
BULK_UPGRADE_INFO_PATTERN: `${EPM_PACKAGES_MANY}/_bulk_upgrade/{taskId}`,
|
||||
LIST_PATTERN: EPM_PACKAGES_MANY,
|
||||
INSTALLED_LIST_PATTERN: EPM_PACKAGES_INSTALLED,
|
||||
LIMITED_LIST_PATTERN: `${EPM_PACKAGES_MANY}/limited`,
|
||||
|
|
|
@ -86,6 +86,14 @@ export const epmRouteService = {
|
|||
return EPM_API_ROUTES.BULK_INSTALL_PATTERN;
|
||||
},
|
||||
|
||||
getBulkUpgradePath: () => {
|
||||
return EPM_API_ROUTES.BULK_UPGRADE_PATTERN;
|
||||
},
|
||||
|
||||
getOneBulkUpgradePath: (taskId: string) => {
|
||||
return EPM_API_ROUTES.BULK_UPGRADE_INFO_PATTERN.replace('{taskId}', taskId);
|
||||
},
|
||||
|
||||
getRemovePath: (pkgName: string, pkgVersion?: string) => {
|
||||
if (pkgVersion) {
|
||||
return EPM_API_ROUTES.DELETE_PATTERN.replace('{pkgName}', pkgName)
|
||||
|
|
|
@ -157,6 +157,23 @@ export interface BulkInstallPackagesResponse {
|
|||
items: Array<BulkInstallPackageInfo | IBulkInstallPackageHTTPError>;
|
||||
}
|
||||
|
||||
export interface BulkUpgradePackagesRequest {
|
||||
packages: Array<{ name: string; version?: string }>;
|
||||
upgrade_package_policies?: boolean;
|
||||
force?: boolean;
|
||||
prerelease?: boolean;
|
||||
}
|
||||
|
||||
export interface BulkUpgradePackagesResponse {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
export interface GetOneBulkUpgradePackagesResponse {
|
||||
status: string;
|
||||
error?: { message: string };
|
||||
results?: Array<{ success?: boolean; error?: { message: string } }>;
|
||||
}
|
||||
|
||||
export interface BulkInstallPackagesRequest {
|
||||
body: {
|
||||
packages: string[];
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 React, { useState } from 'react';
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiConfirmModal,
|
||||
EuiFormRow,
|
||||
EuiSpacer,
|
||||
EuiSwitch,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import type { InstalledPackageUIPackageListItem } from '../types';
|
||||
|
||||
export const ConfirmBulkUpgradeModal: React.FunctionComponent<{
|
||||
selectedItems: InstalledPackageUIPackageListItem[];
|
||||
onClose: () => void;
|
||||
onConfirm: (params: { updatePolicies: boolean }) => void;
|
||||
}> = ({ onClose, onConfirm, selectedItems }) => {
|
||||
const [updatePolicies, setUpdatePolicies] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
return (
|
||||
<EuiConfirmModal
|
||||
title={i18n.translate('xpack.fleet.installedIntegrations.bulkUpgradeModal.title', {
|
||||
defaultMessage: 'Upgrade {countIntegrations} integrations and {countPolicies} policies',
|
||||
values: {
|
||||
countIntegrations: selectedItems.length,
|
||||
countPolicies: selectedItems.reduce(
|
||||
(acc, item) => acc + (item.packagePoliciesInfo?.count ?? 0),
|
||||
0
|
||||
),
|
||||
},
|
||||
})}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.fleet.installedIntegrations.bulkUpgradeModal.confirmButton',
|
||||
{ defaultMessage: 'Upgrade to latest version' }
|
||||
)}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.fleet.installedIntegrations.bulkUpgradeModal.cancelButton',
|
||||
{ defaultMessage: 'Review integration selection' }
|
||||
)}
|
||||
onCancel={onClose}
|
||||
isLoading={isLoading}
|
||||
onConfirm={async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await onConfirm({ updatePolicies });
|
||||
onClose();
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EuiText>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.description"
|
||||
defaultMessage={
|
||||
'We will upgrade your integrations and policies to the latest available version.'
|
||||
}
|
||||
/>
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiSwitch
|
||||
checked={updatePolicies}
|
||||
onChange={(e) => {
|
||||
setUpdatePolicies(e.target.checked);
|
||||
}}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.installedIntegrations.bulkUpgradeModal.policiesSwitchLabel"
|
||||
defaultMessage="Upgrade integration policies"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
iconType="iInCircle"
|
||||
title={i18n.translate(
|
||||
'xpack.fleet.installedIntegrations.bulkUpgradeModal.policiesCallout',
|
||||
{
|
||||
defaultMessage:
|
||||
'When enabled, Fleet will attempt to upgrade and deploy integration policies automatically.',
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</EuiConfirmModal>
|
||||
);
|
||||
};
|
|
@ -14,6 +14,8 @@ import {
|
|||
EuiIcon,
|
||||
EuiCallOut,
|
||||
EuiButton,
|
||||
EuiToolTip,
|
||||
EuiLoadingSpinner,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedDate, FormattedMessage, FormattedTime } from '@kbn/i18n-react';
|
||||
|
||||
|
@ -31,7 +33,7 @@ const InstalledVersionStatus: React.FunctionComponent<{
|
|||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon size="m" type="checkInCircleFilled" color="success" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{item.version}</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{item.installationInfo?.version ?? item.version}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
});
|
||||
|
@ -72,6 +74,34 @@ const UpgradeAvailableVersionStatus: React.FunctionComponent<{
|
|||
);
|
||||
});
|
||||
|
||||
const UpgradingVersionStatus: React.FunctionComponent<{
|
||||
item: InstalledPackageUIPackageListItem;
|
||||
}> = React.memo(({ item }) => {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epmInstalledIntegrations.upgradingTooltip"
|
||||
defaultMessage={'Upgrading to {version}'}
|
||||
values={{ version: item.version }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiLoadingSpinner size={'m'} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epmInstalledIntegrations.upgradingText"
|
||||
defaultMessage="Upgrading..."
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiToolTip>
|
||||
);
|
||||
});
|
||||
|
||||
function formatAttempt(attempt: InstallFailedAttempt): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
|
@ -188,6 +218,8 @@ export const InstallationVersionStatus: React.FunctionComponent<{
|
|||
return <InstalledVersionStatus item={item} />;
|
||||
} else if (status === 'upgrade_available') {
|
||||
return <UpgradeAvailableVersionStatus item={item} />;
|
||||
} else if (status === 'upgrading') {
|
||||
return <UpgradingVersionStatus item={item} />;
|
||||
} else if (status === 'upgrade_failed') {
|
||||
return <InstallUpgradeFailedVersionStatus isUpgradeFailed={true} item={item} />;
|
||||
} else if (status === 'install_failed') {
|
||||
|
|
|
@ -5,13 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import React, { useState, useMemo, useCallback } from 'react';
|
||||
import { EuiButton, EuiContextMenuItem, EuiContextMenuPanel, EuiPopover } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { toMountPoint } from '@kbn/react-kibana-mount';
|
||||
|
||||
import { useStartServices } from '../../../../../../../hooks';
|
||||
import type { InstalledPackageUIPackageListItem } from '../types';
|
||||
import { useBulkActions } from '../hooks/use_bulk_actions';
|
||||
|
||||
import { ConfirmBulkUpgradeModal } from './confirm_bulk_upgrade_modal';
|
||||
|
||||
export const InstalledIntegrationsActionMenu: React.FunctionComponent<{
|
||||
selectedItems: InstalledPackageUIPackageListItem[];
|
||||
}> = ({ selectedItems }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const startServices = useStartServices();
|
||||
|
||||
export const InstalledIntegrationsActionMenu: React.FunctionComponent = () => {
|
||||
const button = (
|
||||
<EuiButton iconType="arrowDown" iconSide="right" disabled onClick={() => {}}>
|
||||
<EuiButton
|
||||
iconType="arrowDown"
|
||||
disabled={selectedItems.length === 0}
|
||||
iconSide="right"
|
||||
onClick={() => setIsOpen((s) => !s)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epmInstalledIntegrations.actionButton"
|
||||
defaultMessage="Actions"
|
||||
|
@ -19,5 +36,65 @@ export const InstalledIntegrationsActionMenu: React.FunctionComponent = () => {
|
|||
</EuiButton>
|
||||
);
|
||||
|
||||
return button;
|
||||
const {
|
||||
actions: { bulkUpgradeIntegrations },
|
||||
} = useBulkActions();
|
||||
|
||||
const openUpgradeModal = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
const ref = startServices.overlays.openModal(
|
||||
toMountPoint(
|
||||
<ConfirmBulkUpgradeModal
|
||||
onClose={() => {
|
||||
ref.close();
|
||||
}}
|
||||
onConfirm={({ updatePolicies }) => bulkUpgradeIntegrations(selectedItems, updatePolicies)}
|
||||
selectedItems={selectedItems}
|
||||
/>,
|
||||
startServices
|
||||
)
|
||||
);
|
||||
}, [selectedItems, startServices, bulkUpgradeIntegrations]);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const hasUpgreadableIntegrations = selectedItems.some(
|
||||
(item) =>
|
||||
item.ui.installation_status === 'upgrade_available' ||
|
||||
item.ui.installation_status === 'upgrade_failed' ||
|
||||
item.ui.installation_status === 'install_failed'
|
||||
);
|
||||
|
||||
return [
|
||||
<EuiContextMenuItem
|
||||
key="upgrade"
|
||||
icon="refresh"
|
||||
disabled={!hasUpgreadableIntegrations}
|
||||
onClick={openUpgradeModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.epmInstalledIntegrations.bulkUpgradeButton"
|
||||
defaultMessage={'Upgrade {count, plural, one {# integration} other {# integrations}}'}
|
||||
values={{
|
||||
count: selectedItems.length,
|
||||
}}
|
||||
/>
|
||||
</EuiContextMenuItem>,
|
||||
<EuiContextMenuItem key="edit" icon="pencil" onClick={() => {}}>
|
||||
TODO other actions
|
||||
</EuiContextMenuItem>,
|
||||
];
|
||||
}, [selectedItems, openUpgradeModal]);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
id="fleet.epmInstalledIntegrations.bulkActionPopover"
|
||||
button={button}
|
||||
isOpen={isOpen}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
panelPaddingSize="none"
|
||||
anchorPosition="downLeft"
|
||||
>
|
||||
<EuiContextMenuPanel size="s" items={items} />
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -19,7 +19,11 @@ import { css } from '@emotion/react';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
|
||||
import type { InstalledIntegrationsFilter, InstalledPackagesUIInstallationStatus } from '../types';
|
||||
import type {
|
||||
InstalledIntegrationsFilter,
|
||||
InstalledPackageUIPackageListItem,
|
||||
InstalledPackagesUIInstallationStatus,
|
||||
} from '../types';
|
||||
import { useAddUrlFilters } from '../hooks/use_url_filters';
|
||||
|
||||
import { InstalledIntegrationsActionMenu } from './installed_integration_action_menu';
|
||||
|
@ -30,7 +34,8 @@ export const InstalledIntegrationsSearchBar: React.FunctionComponent<{
|
|||
filters: InstalledIntegrationsFilter;
|
||||
countPerStatus: { [k: string]: number | undefined };
|
||||
customIntegrationsCount: number;
|
||||
}> = ({ filters, countPerStatus, customIntegrationsCount }) => {
|
||||
selectedItems: InstalledPackageUIPackageListItem[];
|
||||
}> = ({ filters, countPerStatus, customIntegrationsCount, selectedItems }) => {
|
||||
const addUrlFilter = useAddUrlFilters();
|
||||
const theme = useEuiTheme();
|
||||
const [searchTerms, setSearchTerms] = useState(filters.q);
|
||||
|
@ -160,7 +165,7 @@ export const InstalledIntegrationsSearchBar: React.FunctionComponent<{
|
|||
</EuiFilterGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<InstalledIntegrationsActionMenu />
|
||||
<InstalledIntegrationsActionMenu selectedItems={selectedItems} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
EuiBasicTable,
|
||||
EuiLink,
|
||||
|
@ -31,11 +31,14 @@ export const InstalledIntegrationsTable: React.FunctionComponent<{
|
|||
total: number;
|
||||
isLoading: boolean;
|
||||
pagination: UrlPagination;
|
||||
}> = ({ installedPackages, total, isLoading, pagination }) => {
|
||||
selection: {
|
||||
selectedItems: InstalledPackageUIPackageListItem[];
|
||||
setSelectedItems: React.Dispatch<React.SetStateAction<InstalledPackageUIPackageListItem[]>>;
|
||||
};
|
||||
}> = ({ installedPackages, total, isLoading, pagination, selection }) => {
|
||||
const authz = useAuthz();
|
||||
const { getHref } = useLink();
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<InstalledPackageUIPackageListItem[]>([]);
|
||||
const { selectedItems, setSelectedItems } = selection;
|
||||
|
||||
const { setPagination } = pagination;
|
||||
const handleTablePagination = React.useCallback(
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* 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 React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useQueries, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
sendBulkUpgradePackagesForRq,
|
||||
sendGetOneBulkUpgradePackagesForRq,
|
||||
useStartServices,
|
||||
} from '../../../../../../../hooks';
|
||||
|
||||
import type { InstalledPackageUIPackageListItem } from '../types';
|
||||
|
||||
export const bulkActionsContext = React.createContext<{
|
||||
upgradingIntegrations: InstalledPackageUIPackageListItem[];
|
||||
bulkActions: {
|
||||
setBulkUpgradeActions: React.Dispatch<
|
||||
React.SetStateAction<
|
||||
Array<{
|
||||
taskId: string;
|
||||
upgradingIntegrations: InstalledPackageUIPackageListItem[];
|
||||
}>
|
||||
>
|
||||
>;
|
||||
};
|
||||
}>({
|
||||
upgradingIntegrations: [],
|
||||
bulkActions: {
|
||||
setBulkUpgradeActions: () => {},
|
||||
},
|
||||
});
|
||||
|
||||
export const BulkActionContextProvider: React.FunctionComponent<{ children: React.ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useStartServices();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [bulkUpgradeActions, setBulkUpgradeActions] = useState<
|
||||
Array<{
|
||||
taskId: string;
|
||||
upgradingIntegrations: InstalledPackageUIPackageListItem[];
|
||||
}>
|
||||
>([]);
|
||||
|
||||
const upgradingIntegrations = useMemo(() => {
|
||||
return bulkUpgradeActions.flatMap((action) => action.upgradingIntegrations);
|
||||
}, [bulkUpgradeActions]);
|
||||
|
||||
// Poll for upgrade task results
|
||||
useQueries({
|
||||
queries: bulkUpgradeActions.map((action) => ({
|
||||
queryKey: ['bulk-upgrade-packages', action.taskId],
|
||||
queryFn: async () => {
|
||||
const res = await sendGetOneBulkUpgradePackagesForRq(action.taskId);
|
||||
|
||||
if (res.status !== 'pending') {
|
||||
await queryClient.invalidateQueries(['get-packages']);
|
||||
setBulkUpgradeActions((actions) => actions.filter((a) => a.taskId !== action.taskId));
|
||||
|
||||
if (res.status === 'success') {
|
||||
// TODO update copy and view integrations https://github.com/elastic/kibana/issues/209892
|
||||
toasts.addSuccess({
|
||||
title: i18n.translate(
|
||||
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeSuccessTitle',
|
||||
{
|
||||
defaultMessage: 'Upgrade succeeded',
|
||||
}
|
||||
),
|
||||
});
|
||||
} else if (res.status === 'failed') {
|
||||
// TODO update copy and view integrations https://github.com/elastic/kibana/issues/209892
|
||||
toasts.addDanger({
|
||||
title: i18n.translate(
|
||||
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeFailedTitle',
|
||||
{
|
||||
defaultMessage: 'Upgrade failed',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
refetchInterval: 3 * 1000,
|
||||
})),
|
||||
});
|
||||
|
||||
const bulkActions = useMemo(
|
||||
() => ({
|
||||
setBulkUpgradeActions,
|
||||
}),
|
||||
[setBulkUpgradeActions]
|
||||
);
|
||||
|
||||
return (
|
||||
<bulkActionsContext.Provider
|
||||
value={{
|
||||
upgradingIntegrations,
|
||||
bulkActions,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</bulkActionsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export function useBulkActions() {
|
||||
const {
|
||||
upgradingIntegrations,
|
||||
bulkActions: { setBulkUpgradeActions },
|
||||
} = useContext(bulkActionsContext);
|
||||
const {
|
||||
notifications: { toasts },
|
||||
} = useStartServices();
|
||||
|
||||
const bulkUpgradeIntegrations = useCallback(
|
||||
async (items: InstalledPackageUIPackageListItem[], updatePolicies?: boolean) => {
|
||||
try {
|
||||
const res = await sendBulkUpgradePackagesForRq({
|
||||
packages: items.map((item) => ({ name: item.name })),
|
||||
upgrade_package_policies: updatePolicies,
|
||||
});
|
||||
|
||||
setBulkUpgradeActions((actions) => [
|
||||
...actions,
|
||||
{
|
||||
taskId: res.taskId,
|
||||
upgradingIntegrations: items,
|
||||
},
|
||||
]);
|
||||
} catch (error) {
|
||||
toasts.addError(error, {
|
||||
title: i18n.translate(
|
||||
'xpack.fleet.epmInstalledIntegrations.bulkActions.bulkUpgradeErrorTitle',
|
||||
{
|
||||
defaultMessage: 'Error upgrading integrations',
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
},
|
||||
[setBulkUpgradeActions, toasts]
|
||||
);
|
||||
|
||||
return {
|
||||
actions: {
|
||||
bulkUpgradeIntegrations,
|
||||
},
|
||||
upgradingIntegrations,
|
||||
};
|
||||
}
|
|
@ -92,6 +92,31 @@ describe('useInstalledIntegrations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should compute upgrading status in ui property', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useInstalledIntegrations({}, { currentPage: 1, pageSize: 10 }, [
|
||||
{
|
||||
id: 'mysql',
|
||||
name: 'mysql',
|
||||
status: 'installed',
|
||||
version: '2.0.0',
|
||||
installationInfo: { version: '1.0.0' },
|
||||
},
|
||||
] as any)
|
||||
);
|
||||
|
||||
expect(result.current).toEqual(
|
||||
expect.objectContaining({
|
||||
installedPackages: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'mysql',
|
||||
ui: { installation_status: 'upgrading' },
|
||||
}),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should support filtering on status', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useInstalledIntegrations(
|
||||
|
|
|
@ -17,7 +17,13 @@ import type {
|
|||
InstalledPackageUIPackageListItem,
|
||||
} from '../types';
|
||||
|
||||
function getIntegrationStatus(item: PackageListItem): InstalledPackagesUIInstallationStatus {
|
||||
function getIntegrationStatus(
|
||||
item: PackageListItem,
|
||||
isUpgrading: boolean
|
||||
): InstalledPackagesUIInstallationStatus {
|
||||
if (isUpgrading) {
|
||||
return 'upgrading';
|
||||
}
|
||||
if (item.status === 'install_failed') {
|
||||
return 'install_failed';
|
||||
} else if (item.status === 'installed') {
|
||||
|
@ -41,7 +47,8 @@ function getIntegrationStatus(item: PackageListItem): InstalledPackagesUIInstall
|
|||
|
||||
export function useInstalledIntegrations(
|
||||
filters: InstalledIntegrationsFilter,
|
||||
pagination: Pagination
|
||||
pagination: Pagination,
|
||||
upgradingIntegrations?: InstalledPackageUIPackageListItem[]
|
||||
) {
|
||||
const { data, isInitialLoading, isLoading } = useGetPackagesQuery({
|
||||
withPackagePoliciesCount: true,
|
||||
|
@ -55,10 +62,13 @@ export function useInstalledIntegrations(
|
|||
.map((item) => ({
|
||||
...item,
|
||||
ui: {
|
||||
installation_status: getIntegrationStatus(item),
|
||||
installation_status: getIntegrationStatus(
|
||||
item,
|
||||
upgradingIntegrations?.some((u) => u.name === item.name) ?? false
|
||||
),
|
||||
},
|
||||
})),
|
||||
[data]
|
||||
[data, upgradingIntegrations]
|
||||
);
|
||||
|
||||
const localSearch = useLocalSearch(internalInstalledPackages, isInitialLoading);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiSpacer } from '@elastic/eui';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
|
@ -16,6 +16,8 @@ import { InstalledIntegrationsTable } from './components/installed_integrations_
|
|||
import { useInstalledIntegrations } from './hooks/use_installed_integrations';
|
||||
import { useUrlFilters } from './hooks/use_url_filters';
|
||||
import { InstalledIntegrationsSearchBar } from './components/installed_integrations_search_bar';
|
||||
import type { InstalledPackageUIPackageListItem } from './types';
|
||||
import { BulkActionContextProvider, useBulkActions } from './hooks/use_bulk_actions';
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
max-width: 1200px;
|
||||
|
@ -23,10 +25,11 @@ const ContentWrapper = styled.div`
|
|||
height: 100%;
|
||||
`;
|
||||
|
||||
export const InstalledIntegrationsPage: React.FunctionComponent = () => {
|
||||
const InstalledIntegrationsPageContent: React.FunctionComponent = () => {
|
||||
// State management
|
||||
const filters = useUrlFilters();
|
||||
const pagination = useUrlPagination();
|
||||
const { upgradingIntegrations } = useBulkActions();
|
||||
const {
|
||||
installedPackages,
|
||||
countPerStatus,
|
||||
|
@ -34,7 +37,9 @@ export const InstalledIntegrationsPage: React.FunctionComponent = () => {
|
|||
isLoading,
|
||||
isInitialLoading,
|
||||
total,
|
||||
} = useInstalledIntegrations(filters, pagination.pagination);
|
||||
} = useInstalledIntegrations(filters, pagination.pagination, upgradingIntegrations);
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState<InstalledPackageUIPackageListItem[]>([]);
|
||||
|
||||
if (isInitialLoading) {
|
||||
return <Loading />;
|
||||
|
@ -46,6 +51,7 @@ export const InstalledIntegrationsPage: React.FunctionComponent = () => {
|
|||
filters={filters}
|
||||
customIntegrationsCount={customIntegrationsCount}
|
||||
countPerStatus={countPerStatus}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
<InstalledIntegrationsTable
|
||||
|
@ -53,7 +59,16 @@ export const InstalledIntegrationsPage: React.FunctionComponent = () => {
|
|||
pagination={pagination}
|
||||
isLoading={isInitialLoading || isLoading}
|
||||
installedPackages={installedPackages}
|
||||
selection={{ selectedItems, setSelectedItems }}
|
||||
/>
|
||||
</ContentWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstalledIntegrationsPage: React.FunctionComponent = () => {
|
||||
return (
|
||||
<BulkActionContextProvider>
|
||||
<InstalledIntegrationsPageContent />
|
||||
</BulkActionContextProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ export type InstalledPackagesUIInstallationStatus =
|
|||
| 'installed'
|
||||
| 'install_failed'
|
||||
| 'upgrade_failed'
|
||||
| 'upgrading'
|
||||
| 'upgrade_available';
|
||||
|
||||
export type InstalledPackageUIPackageListItem = PackageListItem & {
|
||||
|
|
|
@ -31,8 +31,11 @@ import type {
|
|||
GetInputsTemplatesResponse,
|
||||
} from '../../types';
|
||||
import type {
|
||||
BulkUpgradePackagesRequest,
|
||||
BulkUpgradePackagesResponse,
|
||||
FleetErrorResponse,
|
||||
GetEpmDataStreamsResponse,
|
||||
GetOneBulkUpgradePackagesResponse,
|
||||
GetStatsResponse,
|
||||
} from '../../../common/types';
|
||||
import { API_VERSIONS } from '../../../common/constants';
|
||||
|
@ -305,6 +308,23 @@ export const sendBulkInstallPackages = (
|
|||
});
|
||||
};
|
||||
|
||||
export const sendBulkUpgradePackagesForRq = (params: BulkUpgradePackagesRequest) => {
|
||||
return sendRequestForRq<BulkUpgradePackagesResponse>({
|
||||
path: epmRouteService.getBulkUpgradePath(),
|
||||
method: 'post',
|
||||
version: API_VERSIONS.public.v1,
|
||||
body: params,
|
||||
});
|
||||
};
|
||||
|
||||
export const sendGetOneBulkUpgradePackagesForRq = (taskId: string) => {
|
||||
return sendRequestForRq<GetOneBulkUpgradePackagesResponse>({
|
||||
path: epmRouteService.getOneBulkUpgradePath(taskId),
|
||||
method: 'get',
|
||||
version: API_VERSIONS.public.v1,
|
||||
});
|
||||
};
|
||||
|
||||
export function sendRemovePackage(
|
||||
{ pkgName, pkgVersion }: DeletePackageRequest['params'],
|
||||
query?: DeletePackageRequest['query']
|
||||
|
|
|
@ -150,6 +150,7 @@ import { registerBumpAgentPoliciesTask } from './services/agent_policies/bump_ag
|
|||
import { UpgradeAgentlessDeploymentsTask } from './tasks/upgrade_agentless_deployment';
|
||||
import { SyncIntegrationsTask } from './tasks/sync_integrations/sync_integrations_task';
|
||||
import { AutomaticAgentUpgradeTask } from './tasks/automatic_agent_upgrade_task';
|
||||
import { registerBulkUpgradePackagesTask } from './tasks/bulk_upgrade_packages_task';
|
||||
|
||||
export interface FleetSetupDeps {
|
||||
security: SecurityPluginSetup;
|
||||
|
@ -639,6 +640,7 @@ export class FleetPlugin
|
|||
registerUpgradeManagedPackagePoliciesTask(deps.taskManager);
|
||||
registerDeployAgentPoliciesTask(deps.taskManager);
|
||||
registerBumpAgentPoliciesTask(deps.taskManager);
|
||||
registerBulkUpgradePackagesTask(deps.taskManager);
|
||||
|
||||
this.bulkActionsResolver = new BulkActionsResolver(deps.taskManager, core);
|
||||
this.checkDeletedFilesTask = new CheckDeletedFilesTask({
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* 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 { appContextService } from '../../services';
|
||||
import type {
|
||||
BulkUpgradePackagesRequestSchema,
|
||||
FleetRequestHandler,
|
||||
GetOneBulkUpgradePackagesRequestSchema,
|
||||
} from '../../types';
|
||||
import { HTTPAuthorizationHeader } from '../../../common/http_authorization_header';
|
||||
|
||||
import type {
|
||||
BulkUpgradePackagesResponse,
|
||||
GetOneBulkUpgradePackagesResponse,
|
||||
} from '../../../common/types';
|
||||
import {
|
||||
getBulkUpgradeTaskResults,
|
||||
scheduleBulkUpgrade,
|
||||
} from '../../tasks/bulk_upgrade_packages_task';
|
||||
import { getInstallationsByName } from '../../services/epm/packages/get';
|
||||
import { FleetError } from '../../errors';
|
||||
|
||||
export const postBulkUpgradePackagesHandler: FleetRequestHandler<
|
||||
undefined,
|
||||
undefined,
|
||||
TypeOf<typeof BulkUpgradePackagesRequestSchema.body>
|
||||
> = async (context, request, response) => {
|
||||
const fleetContext = await context.fleet;
|
||||
const savedObjectsClient = fleetContext.internalSoClient;
|
||||
const spaceId = fleetContext.spaceId;
|
||||
const user = appContextService.getSecurityCore().authc.getCurrentUser(request) || undefined;
|
||||
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username);
|
||||
|
||||
const taskManagerStart = appContextService.getTaskManagerStart();
|
||||
if (!taskManagerStart) {
|
||||
throw new Error('Task manager not defined');
|
||||
}
|
||||
const pkgNames = request.body.packages.map(({ name }) => name);
|
||||
const installations = await getInstallationsByName({ savedObjectsClient, pkgNames });
|
||||
|
||||
const nonInstalledPackages = pkgNames.filter(
|
||||
(pkgName) => !installations.some((installation) => installation.name === pkgName)
|
||||
);
|
||||
if (nonInstalledPackages.length) {
|
||||
throw new FleetError(
|
||||
`Cannot upgrade non installed packages: ${nonInstalledPackages.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
const taskId = await scheduleBulkUpgrade(taskManagerStart, savedObjectsClient, {
|
||||
authorizationHeader,
|
||||
spaceId,
|
||||
packages: request.body.packages,
|
||||
upgradePackagePolicies: request.body.upgrade_package_policies,
|
||||
force: request.body.force,
|
||||
prerelease: request.body.prerelease,
|
||||
});
|
||||
|
||||
const body: BulkUpgradePackagesResponse = {
|
||||
taskId,
|
||||
};
|
||||
return response.ok({ body });
|
||||
};
|
||||
|
||||
export const getOneBulkUpgradePackagesHandler: FleetRequestHandler<
|
||||
TypeOf<typeof GetOneBulkUpgradePackagesRequestSchema.params>
|
||||
> = async (context, request, response) => {
|
||||
const taskManagerStart = appContextService.getTaskManagerStart();
|
||||
if (!taskManagerStart) {
|
||||
throw new Error('Task manager not defined');
|
||||
}
|
||||
|
||||
const results = await getBulkUpgradeTaskResults(taskManagerStart, request.params.taskId);
|
||||
const body: GetOneBulkUpgradePackagesResponse = {
|
||||
status: results.status,
|
||||
error: results.error,
|
||||
results: results.results,
|
||||
};
|
||||
return response.ok({ body });
|
||||
};
|
|
@ -7,8 +7,8 @@
|
|||
|
||||
import type { RouteSecurity } from '@kbn/core-http-server';
|
||||
|
||||
import { parseExperimentalConfigValue } from '../../../common/experimental_features';
|
||||
import { API_VERSIONS } from '../../../common/constants';
|
||||
|
||||
import type { FleetAuthz } from '../../../common';
|
||||
|
||||
import {
|
||||
|
@ -55,6 +55,10 @@ import {
|
|||
GetDataStreamsResponseSchema,
|
||||
GetBulkAssetsResponseSchema,
|
||||
ReauthorizeTransformResponseSchema,
|
||||
BulkUpgradePackagesRequestSchema,
|
||||
BulkUpgradePackagesResponseSchema,
|
||||
GetOneBulkUpgradePackagesRequestSchema,
|
||||
GetOneBulkUpgradePackagesResponseSchema,
|
||||
} from '../../types';
|
||||
import type { FleetConfigType } from '../../config';
|
||||
import { FLEET_API_PRIVILEGES } from '../../constants/api_privileges';
|
||||
|
@ -84,6 +88,7 @@ import {
|
|||
deletePackageKibanaAssetsHandler,
|
||||
installPackageKibanaAssetsHandler,
|
||||
} from './kibana_assets_handler';
|
||||
import { postBulkUpgradePackagesHandler, getOneBulkUpgradePackagesHandler } from './bulk_handler';
|
||||
|
||||
const MAX_FILE_SIZE_BYTES = 104857600; // 100MB
|
||||
|
||||
|
@ -119,6 +124,8 @@ export const READ_PACKAGE_INFO_SECURITY: RouteSecurity = {
|
|||
};
|
||||
|
||||
export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => {
|
||||
const experimentalFeatures = parseExperimentalConfigValue(config.enableExperimental);
|
||||
|
||||
router.versioned
|
||||
.get({
|
||||
path: EPM_API_ROUTES.CATEGORIES_PATTERN,
|
||||
|
@ -446,6 +453,62 @@ export const registerRoutes = (router: FleetAuthzRouter, config: FleetConfigType
|
|||
deletePackageKibanaAssetsHandler
|
||||
);
|
||||
|
||||
if (experimentalFeatures.installedIntegrationsTabularUI) {
|
||||
router.versioned
|
||||
.post({
|
||||
path: EPM_API_ROUTES.BULK_UPGRADE_PATTERN,
|
||||
security: INSTALL_PACKAGES_SECURITY,
|
||||
summary: `Bulk upgrade packages`,
|
||||
options: {
|
||||
tags: ['oas-tag:Elastic Package Manager (EPM)'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: BulkUpgradePackagesRequestSchema,
|
||||
response: {
|
||||
200: {
|
||||
body: () => BulkUpgradePackagesResponseSchema,
|
||||
},
|
||||
400: {
|
||||
body: genericErrorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
postBulkUpgradePackagesHandler
|
||||
);
|
||||
|
||||
router.versioned
|
||||
.get({
|
||||
path: EPM_API_ROUTES.BULK_UPGRADE_INFO_PATTERN,
|
||||
security: INSTALL_PACKAGES_SECURITY,
|
||||
summary: `Get Bulk upgrade packages details`,
|
||||
options: {
|
||||
tags: ['oas-tag:Elastic Package Manager (EPM)'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: GetOneBulkUpgradePackagesRequestSchema,
|
||||
response: {
|
||||
200: {
|
||||
body: () => GetOneBulkUpgradePackagesResponseSchema,
|
||||
},
|
||||
400: {
|
||||
body: genericErrorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getOneBulkUpgradePackagesHandler
|
||||
);
|
||||
}
|
||||
|
||||
router.versioned
|
||||
.post({
|
||||
path: EPM_API_ROUTES.BULK_INSTALL_PATTERN,
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { createAppContextStartContractMock } from '../mocks';
|
||||
import { appContextService } from '../services';
|
||||
import { packagePolicyService } from '../services/package_policy';
|
||||
import { installPackage } from '../services/epm/packages';
|
||||
|
||||
import { _runBulkUpgradeTask } from './bulk_upgrade_packages_task';
|
||||
|
||||
jest.mock('../services/epm/packages');
|
||||
jest.mock('../services/package_policy');
|
||||
|
||||
describe('Bulk upgrade task', () => {
|
||||
beforeEach(() => {
|
||||
const mockContract = createAppContextStartContractMock();
|
||||
appContextService.start(mockContract);
|
||||
|
||||
jest.mocked(installPackage).mockReset();
|
||||
jest.mocked(installPackage).mockImplementation(async (params) => {
|
||||
if (!('pkgkey' in params)) {
|
||||
throw new Error('Invalid call to installPackage');
|
||||
}
|
||||
|
||||
if (params.pkgkey.startsWith('test_valid')) {
|
||||
return {
|
||||
status: 'installed',
|
||||
} as any;
|
||||
}
|
||||
|
||||
if (params.pkgkey.startsWith('test_invalid')) {
|
||||
return {
|
||||
error: new Error('Impossible to install: ' + params.pkgkey),
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error('not implemented');
|
||||
});
|
||||
|
||||
jest.mocked(packagePolicyService.listIds).mockResolvedValue({ items: ['id1', 'id2'] } as any);
|
||||
|
||||
jest
|
||||
.mocked(packagePolicyService.bulkUpgrade)
|
||||
.mockResolvedValue([{ success: true }, { success: true }] as any);
|
||||
});
|
||||
describe('_runBulkUpgradeTask', () => {
|
||||
it('should work for successfull upgrade', async () => {
|
||||
const res = await _runBulkUpgradeTask({
|
||||
abortController: new AbortController(),
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
taskParams: { packages: [{ name: 'test_valid' }], authorizationHeader: null },
|
||||
});
|
||||
|
||||
expect(installPackage).toBeCalled();
|
||||
|
||||
expect(res).toEqual([{ name: 'test_valid', success: true }]);
|
||||
});
|
||||
|
||||
it('should return error for non successful upgrade', async () => {
|
||||
const res = await _runBulkUpgradeTask({
|
||||
abortController: new AbortController(),
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
taskParams: {
|
||||
packages: [
|
||||
{ name: 'test_valid_1' },
|
||||
{ name: 'test_invalid_1' },
|
||||
{ name: 'test_valid_2' },
|
||||
{ name: 'test_invalid_2' },
|
||||
],
|
||||
authorizationHeader: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(installPackage).toBeCalledTimes(4);
|
||||
expect(res).toEqual([
|
||||
{ name: 'test_valid_1', success: true },
|
||||
{
|
||||
name: 'test_invalid_1',
|
||||
success: false,
|
||||
error: { message: 'Impossible to install: test_invalid_1' },
|
||||
},
|
||||
{ name: 'test_valid_2', success: true },
|
||||
{
|
||||
name: 'test_invalid_2',
|
||||
success: false,
|
||||
error: { message: 'Impossible to install: test_invalid_2' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should work for successful upgrade with package policies upgrade', async () => {
|
||||
const res = await _runBulkUpgradeTask({
|
||||
abortController: new AbortController(),
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
taskParams: {
|
||||
packages: [{ name: 'test_valid' }],
|
||||
authorizationHeader: null,
|
||||
upgradePackagePolicies: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(res).toEqual([{ name: 'test_valid', success: true }]);
|
||||
|
||||
expect(installPackage).toBeCalled();
|
||||
expect(packagePolicyService.bulkUpgrade).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not continue to upgrade packages when task is cancelled', async () => {
|
||||
const abortController = new AbortController();
|
||||
abortController.abort();
|
||||
await expect(() =>
|
||||
_runBulkUpgradeTask({
|
||||
abortController,
|
||||
logger: loggingSystemMock.createLogger(),
|
||||
taskParams: {
|
||||
packages: [
|
||||
{ name: 'test_valid_1' },
|
||||
{ name: 'test_invalid_1' },
|
||||
{ name: 'test_valid_2' },
|
||||
{ name: 'test_invalid_2' },
|
||||
],
|
||||
authorizationHeader: null,
|
||||
},
|
||||
})
|
||||
).rejects.toThrow(/Task was aborted/);
|
||||
|
||||
expect(installPackage).toBeCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common';
|
||||
import type {
|
||||
ConcreteTaskInstance,
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '@kbn/task-manager-plugin/server';
|
||||
import type { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
|
||||
import { HTTPAuthorizationHeader } from '../../common/http_authorization_header';
|
||||
import { installPackage } from '../services/epm/packages';
|
||||
import { appContextService, packagePolicyService } from '../services';
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../constants';
|
||||
|
||||
const TASK_TYPE = 'fleet:packages-bulk-operations';
|
||||
const TASK_TITLE = 'Fleet packages bulk operations';
|
||||
const TASK_TIMEOUT = '10m';
|
||||
|
||||
interface BulkUpgradeTaskParams {
|
||||
packages: Array<{ name: string; version?: string }>;
|
||||
spaceId?: string;
|
||||
authorizationHeader: HTTPAuthorizationHeader | null;
|
||||
force?: boolean;
|
||||
prerelease?: boolean;
|
||||
upgradePackagePolicies?: boolean;
|
||||
}
|
||||
|
||||
interface BulkUpgradeTaskState {
|
||||
isDone?: boolean;
|
||||
error?: { message: string };
|
||||
results?: Array<
|
||||
| {
|
||||
success: true;
|
||||
name: string;
|
||||
}
|
||||
| { success: false; name: string; error: { message: string } }
|
||||
>;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export function registerBulkUpgradePackagesTask(taskManager: TaskManagerSetupContract) {
|
||||
taskManager.registerTaskDefinitions({
|
||||
[TASK_TYPE]: {
|
||||
title: TASK_TITLE,
|
||||
timeout: TASK_TIMEOUT,
|
||||
createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
return {
|
||||
run: async () => {
|
||||
const logger = appContextService.getLogger();
|
||||
if (taskInstance.state.isDone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskParams = taskInstance.params as BulkUpgradeTaskParams;
|
||||
try {
|
||||
const results = await _runBulkUpgradeTask({ abortController, logger, taskParams });
|
||||
const state: BulkUpgradeTaskState = {
|
||||
isDone: true,
|
||||
results,
|
||||
};
|
||||
return {
|
||||
runAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
state,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Packages bulk upgrade failed', { error });
|
||||
return {
|
||||
runAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
state: {
|
||||
isDone: true,
|
||||
error: formatError(error),
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
cancel: async () => {
|
||||
abortController.abort('task timed out');
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function formatError(err: Error) {
|
||||
return { message: err.message };
|
||||
}
|
||||
|
||||
export async function _runBulkUpgradeTask({
|
||||
abortController,
|
||||
taskParams,
|
||||
logger,
|
||||
}: {
|
||||
taskParams: BulkUpgradeTaskParams;
|
||||
abortController: AbortController;
|
||||
logger: Logger;
|
||||
}) {
|
||||
const {
|
||||
packages,
|
||||
spaceId = DEFAULT_SPACE_ID,
|
||||
authorizationHeader,
|
||||
force,
|
||||
prerelease,
|
||||
upgradePackagePolicies,
|
||||
} = taskParams;
|
||||
const esClient = appContextService.getInternalUserESClient();
|
||||
const savedObjectsClient = appContextService.getInternalUserSOClientForSpaceId(spaceId);
|
||||
|
||||
const results: BulkUpgradeTaskState['results'] = [];
|
||||
|
||||
for (const pkg of packages) {
|
||||
// Throw between package install if task is aborted
|
||||
if (abortController.signal.aborted) {
|
||||
throw new Error('Task was aborted');
|
||||
}
|
||||
try {
|
||||
const installResult = await installPackage({
|
||||
spaceId,
|
||||
authorizationHeader: authorizationHeader
|
||||
? new HTTPAuthorizationHeader(
|
||||
authorizationHeader.scheme,
|
||||
authorizationHeader.credentials,
|
||||
authorizationHeader.username
|
||||
)
|
||||
: undefined,
|
||||
installSource: 'registry', // Upgrade can only happens from the registry,
|
||||
esClient,
|
||||
savedObjectsClient,
|
||||
pkgkey: pkg?.version ? `${pkg.name}-${pkg.version}` : pkg.name,
|
||||
force,
|
||||
prerelease,
|
||||
});
|
||||
|
||||
if (installResult.error) {
|
||||
throw installResult.error;
|
||||
}
|
||||
|
||||
if (upgradePackagePolicies) {
|
||||
await bulkUpgradePackagePolicies({
|
||||
savedObjectsClient,
|
||||
esClient,
|
||||
pkgName: pkg.name,
|
||||
});
|
||||
}
|
||||
|
||||
results.push({
|
||||
name: pkg.name,
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Upgrade of package: ${pkg.name} failed`, { error });
|
||||
results.push({
|
||||
name: pkg.name,
|
||||
success: false,
|
||||
error: formatError(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function bulkUpgradePackagePolicies({
|
||||
savedObjectsClient,
|
||||
esClient,
|
||||
pkgName,
|
||||
}: {
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
esClient: ElasticsearchClient;
|
||||
pkgName: string;
|
||||
}) {
|
||||
const policyIdsToUpgrade = await packagePolicyService.listIds(savedObjectsClient, {
|
||||
page: 1,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name:${pkgName}`,
|
||||
});
|
||||
|
||||
if (policyIdsToUpgrade.items.length) {
|
||||
const upgradePackagePoliciesResults = await packagePolicyService.bulkUpgrade(
|
||||
savedObjectsClient,
|
||||
esClient,
|
||||
policyIdsToUpgrade.items
|
||||
);
|
||||
const errors = upgradePackagePoliciesResults
|
||||
.filter((result) => !result.success)
|
||||
.map((result) => `${result.statusCode}: ${result.body?.message ?? ''}`);
|
||||
if (errors.length) {
|
||||
throw new Error(`Package policies upgrade for ${pkgName} failed:\n${errors.join('\n')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function scheduleBulkUpgrade(
|
||||
taskManagerStart: TaskManagerStartContract,
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
taskParams: BulkUpgradeTaskParams
|
||||
) {
|
||||
const id = uuidv4();
|
||||
await taskManagerStart.ensureScheduled({
|
||||
id: `${TASK_TYPE}:${id}`,
|
||||
scope: ['fleet'],
|
||||
params: taskParams,
|
||||
taskType: TASK_TYPE,
|
||||
runAt: new Date(Date.now() + 3 * 1000),
|
||||
state: {},
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
export async function getBulkUpgradeTaskResults(
|
||||
taskManagerStart: TaskManagerStartContract,
|
||||
id: string
|
||||
) {
|
||||
const task = await taskManagerStart.get(`${TASK_TYPE}:${id}`);
|
||||
const state: BulkUpgradeTaskState = task.state;
|
||||
const status = !state?.isDone
|
||||
? 'pending'
|
||||
: state?.error || state?.results?.some((r) => !r.success)
|
||||
? 'failed'
|
||||
: 'success';
|
||||
return {
|
||||
status,
|
||||
error: state.error,
|
||||
results: state.results,
|
||||
};
|
||||
}
|
|
@ -369,6 +369,22 @@ export const BulkInstallPackagesFromRegistryResponseSchema = schema.object({
|
|||
items: schema.arrayOf(BulkInstallPackagesResponseItemSchema),
|
||||
});
|
||||
|
||||
export const BulkUpgradePackagesResponseSchema = schema.object({ taskId: schema.string() });
|
||||
|
||||
export const GetOneBulkUpgradePackagesResponseSchema = schema.object({
|
||||
status: schema.string(),
|
||||
error: schema.maybe(schema.object({ message: schema.string() })),
|
||||
results: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
name: schema.string(),
|
||||
success: schema.boolean(),
|
||||
error: schema.maybe(schema.object({ message: schema.string() })),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
export const DeletePackageResponseSchema = schema.object({
|
||||
items: schema.arrayOf(AssetReferenceSchema),
|
||||
});
|
||||
|
@ -549,6 +565,27 @@ export const BulkInstallPackagesFromRegistryRequestSchema = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const GetOneBulkUpgradePackagesRequestSchema = {
|
||||
params: schema.object({
|
||||
taskId: schema.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export const BulkUpgradePackagesRequestSchema = {
|
||||
body: schema.object({
|
||||
packages: schema.arrayOf(
|
||||
schema.object({
|
||||
name: schema.string(),
|
||||
version: schema.maybe(schema.string()),
|
||||
}),
|
||||
{ minSize: 1 }
|
||||
),
|
||||
prerelease: schema.maybe(schema.boolean()),
|
||||
force: schema.boolean({ defaultValue: false }),
|
||||
upgrade_package_policies: schema.boolean({ defaultValue: false }),
|
||||
}),
|
||||
};
|
||||
|
||||
export const InstallPackageByUploadRequestSchema = {
|
||||
query: schema.object({
|
||||
ignoreMappingUpdateErrors: schema.boolean({ defaultValue: false }),
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* 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 {
|
||||
BulkInstallPackageInfo,
|
||||
BulkInstallPackagesResponse,
|
||||
IBulkInstallPackageHTTPError,
|
||||
} from '@kbn/fleet-plugin/common';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const fleetAndAgents = getService('fleetAndAgents');
|
||||
|
||||
const deletePackage = async (name: string, version: string) => {
|
||||
await supertest.delete(`/api/fleet/epm/packages/${name}/${version}`).set('kbn-xsrf', 'xxxx');
|
||||
};
|
||||
|
||||
describe('bulk package upgrade using install api', () => {
|
||||
skipIfNoDockerRegistry(providerContext);
|
||||
|
||||
before(async () => {
|
||||
await fleetAndAgents.setup();
|
||||
});
|
||||
|
||||
describe('bulk package upgrade with a package already installed', () => {
|
||||
beforeEach(async () => {
|
||||
await supertest
|
||||
.post(`/api/fleet/epm/packages/multiple_versions/0.1.0`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await deletePackage('multiple_versions', '0.1.0');
|
||||
await deletePackage('multiple_versions', '0.3.0');
|
||||
await deletePackage('overrides', '0.1.0');
|
||||
});
|
||||
|
||||
it('should return 400 if no packages are requested for upgrade', async function () {
|
||||
await supertest.post(`/api/fleet/epm/packages/_bulk`).set('kbn-xsrf', 'xxxx').expect(400);
|
||||
});
|
||||
it('should return 403 if user without integrations all requests upgrade', async function () {
|
||||
await supertestWithoutAuth
|
||||
.post(`/api/fleet/epm/packages/_bulk`)
|
||||
.auth(testUsers.fleet_all_int_read.username, testUsers.fleet_all_int_read.password)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'overrides'] })
|
||||
.expect(403);
|
||||
});
|
||||
it('should return 403 if user without fleet access requests upgrade', async function () {
|
||||
await supertestWithoutAuth
|
||||
.post(`/api/fleet/epm/packages/_bulk`)
|
||||
.auth(testUsers.integr_all_only.username, testUsers.integr_all_only.password)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'overrides'] })
|
||||
.expect(403);
|
||||
});
|
||||
it('should return 200 and an array for upgrading a package', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(1);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
const entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
});
|
||||
it('should return an error for packages that do not exist', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'blahblah'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(2);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
const entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
|
||||
const err = body.items[1] as IBulkInstallPackageHTTPError;
|
||||
expect(err.statusCode).equal(404);
|
||||
expect(body.items[1].name).equal('blahblah');
|
||||
});
|
||||
it('should upgrade multiple packages', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'overrides'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(2);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
let entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
|
||||
entry = body.items[1] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.1.0');
|
||||
expect(entry.name).equal('overrides');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk upgrade without package already installed', () => {
|
||||
afterEach(async () => {
|
||||
await deletePackage('multiple_versions', '0.3.0');
|
||||
});
|
||||
|
||||
it('should return 200 and an array for upgrading a package', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(1);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
const entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -6,123 +6,209 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import {
|
||||
BulkInstallPackageInfo,
|
||||
BulkInstallPackagesResponse,
|
||||
IBulkInstallPackageHTTPError,
|
||||
} from '@kbn/fleet-plugin/common';
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../../helpers';
|
||||
import { testUsers } from '../test_users';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const fleetAndAgents = getService('fleetAndAgents');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
const deletePackage = async (name: string, version: string) => {
|
||||
await supertest.delete(`/api/fleet/epm/packages/${name}/${version}`).set('kbn-xsrf', 'xxxx');
|
||||
};
|
||||
|
||||
describe('bulk package upgrade api', () => {
|
||||
const installPackage = async (name: string, version: string) => {
|
||||
await supertest
|
||||
.post(`/api/fleet/epm/packages/${name}/${version}`)
|
||||
.send({ force: true })
|
||||
.set('kbn-xsrf', 'xxxx');
|
||||
};
|
||||
|
||||
describe('packages/_bulk_upgrade', () => {
|
||||
skipIfNoDockerRegistry(providerContext);
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await fleetAndAgents.setup();
|
||||
});
|
||||
|
||||
describe('bulk package upgrade with a package already installed', () => {
|
||||
beforeEach(async () => {
|
||||
await supertest
|
||||
.post(`/api/fleet/epm/packages/multiple_versions/0.1.0`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
});
|
||||
afterEach(async () => {
|
||||
await deletePackage('multiple_versions', '0.1.0');
|
||||
await deletePackage('multiple_versions', '0.3.0');
|
||||
await deletePackage('overrides', '0.1.0');
|
||||
});
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should return 400 if no packages are requested for upgrade', async function () {
|
||||
await supertest.post(`/api/fleet/epm/packages/_bulk`).set('kbn-xsrf', 'xxxx').expect(400);
|
||||
});
|
||||
it('should return 403 if user without integrations all requests upgrade', async function () {
|
||||
await supertestWithoutAuth
|
||||
.post(`/api/fleet/epm/packages/_bulk`)
|
||||
.auth(testUsers.fleet_all_int_read.username, testUsers.fleet_all_int_read.password)
|
||||
describe('Validations', () => {
|
||||
it('should not allow to create a _bulk_upgrade with non installed packages', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk_upgrade`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'overrides'] })
|
||||
.expect(403);
|
||||
});
|
||||
it('should return 403 if user without fleet access requests upgrade', async function () {
|
||||
await supertestWithoutAuth
|
||||
.post(`/api/fleet/epm/packages/_bulk`)
|
||||
.auth(testUsers.integr_all_only.username, testUsers.integr_all_only.password)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'overrides'] })
|
||||
.expect(403);
|
||||
});
|
||||
it('should return 200 and an array for upgrading a package', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(1);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
const entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
});
|
||||
it('should return an error for packages that do not exist', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'blahblah'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(2);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
const entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
.send({ packages: [{ name: 'idonotexists' }] })
|
||||
.expect(400);
|
||||
|
||||
const err = body.items[1] as IBulkInstallPackageHTTPError;
|
||||
expect(err.statusCode).equal(404);
|
||||
expect(body.items[1].name).equal('blahblah');
|
||||
});
|
||||
it('should upgrade multiple packages', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions', 'overrides'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(2);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
let entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
|
||||
entry = body.items[1] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.1.0');
|
||||
expect(entry.name).equal('overrides');
|
||||
expect(res.body.message).equal('Cannot upgrade non installed packages: idonotexists');
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulk upgrade without package already installed', () => {
|
||||
afterEach(async () => {
|
||||
await deletePackage('multiple_versions', '0.3.0');
|
||||
describe('Success upgrade', () => {
|
||||
const POLICIES_IDS = ['multiple-versions-1', 'multiple-versions-2'];
|
||||
beforeEach(async () => {
|
||||
await installPackage('multiple_versions', '0.1.0');
|
||||
// Create package policies
|
||||
for (const id of POLICIES_IDS) {
|
||||
await supertest
|
||||
.post(`/api/fleet/package_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
id,
|
||||
policy_ids: [],
|
||||
package: {
|
||||
name: 'multiple_versions',
|
||||
version: '0.1.0',
|
||||
},
|
||||
name: id,
|
||||
description: '',
|
||||
namespace: '',
|
||||
inputs: {},
|
||||
})
|
||||
.expect(200);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return 200 and an array for upgrading a package', async function () {
|
||||
const { body }: { body: BulkInstallPackagesResponse } = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk?prerelease=true`)
|
||||
afterEach(async () => {
|
||||
await deletePackage('multiple_versions', '0.2.0');
|
||||
for (const id of POLICIES_IDS) {
|
||||
await supertest
|
||||
.delete(`/api/fleet/package_policies/${id}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
}
|
||||
});
|
||||
|
||||
async function assertPackagePoliciesVersion(expectedVersion: string) {
|
||||
for (const id of POLICIES_IDS) {
|
||||
const res = await supertest
|
||||
.get(`/api/fleet/package_policies/${id}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
|
||||
expect(res.body.item.package.version).equal(expectedVersion);
|
||||
}
|
||||
}
|
||||
|
||||
async function assetPackageInstallVersion(expectedVersion: string) {
|
||||
const res = await supertest
|
||||
.get(`/api/fleet/epm/packages/multiple_versions`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: ['multiple_versions'] })
|
||||
.expect(200);
|
||||
expect(body.items.length).equal(1);
|
||||
expect(body.items[0].name).equal('multiple_versions');
|
||||
const entry = body.items[0] as BulkInstallPackageInfo;
|
||||
expect(entry.version).equal('0.3.0');
|
||||
|
||||
expect(res.body.item.installationInfo.version).equal(expectedVersion);
|
||||
}
|
||||
|
||||
it('should allow to create a _bulk_upgrade with installed packages that will succeed', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk_upgrade`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
packages: [{ name: 'multiple_versions', version: '0.2.0' }],
|
||||
force: true,
|
||||
prerelease: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const maxTimeout = Date.now() + 60 * 1000;
|
||||
let lastPollResult: string = '';
|
||||
while (Date.now() < maxTimeout) {
|
||||
const pollRes = await supertest
|
||||
.get(`/api/fleet/epm/packages/_bulk_upgrade/${res.body.taskId}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
if (pollRes.body.status === 'success') {
|
||||
await assetPackageInstallVersion('0.2.0');
|
||||
await assertPackagePoliciesVersion('0.1.0');
|
||||
return;
|
||||
}
|
||||
|
||||
lastPollResult = JSON.stringify(pollRes.body);
|
||||
}
|
||||
|
||||
throw new Error(`bulk upgrade of "multiple_versions" never succeed: ${lastPollResult}`);
|
||||
});
|
||||
|
||||
it('should upgrade related policies if upgrade_package_policies:true', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk_upgrade`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
packages: [{ name: 'multiple_versions', version: '0.2.0' }],
|
||||
prerelease: true,
|
||||
force: true,
|
||||
upgrade_package_policies: true,
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const maxTimeout = Date.now() + 60 * 1000;
|
||||
let lastPollResult: string = '';
|
||||
while (Date.now() < maxTimeout) {
|
||||
const pollRes = await supertest
|
||||
.get(`/api/fleet/epm/packages/_bulk_upgrade/${res.body.taskId}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
if (pollRes.body.status === 'success') {
|
||||
await assertPackagePoliciesVersion('0.2.0');
|
||||
await assetPackageInstallVersion('0.2.0');
|
||||
return;
|
||||
}
|
||||
|
||||
lastPollResult = JSON.stringify(pollRes.body);
|
||||
}
|
||||
|
||||
throw new Error(`bulk upgrade of "multiple_versions" never succeed: ${lastPollResult}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Failed upgrade', () => {
|
||||
const goodPackageVersion = '0.1.0';
|
||||
const badPackageVersion = '0.2.0';
|
||||
beforeEach(async () => {
|
||||
await installPackage('error_handling', goodPackageVersion);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await deletePackage('error_handling', goodPackageVersion);
|
||||
});
|
||||
|
||||
it('should allow to create a _bulk_upgrade with installed packages that will fail', async () => {
|
||||
const res = await supertest
|
||||
.post(`/api/fleet/epm/packages/_bulk_upgrade`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({ packages: [{ name: 'error_handling', version: badPackageVersion }] })
|
||||
.expect(200);
|
||||
|
||||
const maxTimeout = Date.now() + 60 * 1000;
|
||||
let lastPollResult: string = '';
|
||||
while (Date.now() < maxTimeout) {
|
||||
const pollRes = await supertest
|
||||
.get(`/api/fleet/epm/packages/_bulk_upgrade/${res.body.taskId}`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.expect(200);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
|
||||
if (pollRes.body.status === 'failed') {
|
||||
return;
|
||||
}
|
||||
|
||||
lastPollResult = JSON.stringify(pollRes.body);
|
||||
}
|
||||
|
||||
throw new Error(`bulk upgrade of "multiple_versions" never failed: ${lastPollResult}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@ export default function loadTests({ loadTestFile, getService }) {
|
|||
loadTestFile(require.resolve('./install_tag_assets'));
|
||||
loadTestFile(require.resolve('./install_with_streaming'));
|
||||
loadTestFile(require.resolve('./bulk_upgrade'));
|
||||
loadTestFile(require.resolve('./bulk_install_upgrade'));
|
||||
loadTestFile(require.resolve('./bulk_install'));
|
||||
loadTestFile(require.resolve('./update_assets'));
|
||||
loadTestFile(require.resolve('./data_stream'));
|
||||
|
|
|
@ -85,7 +85,10 @@ export default async function ({ readConfigFile, log }: FtrConfigProviderContext
|
|||
'./apis/fixtures/package_verification/signatures/fleet_test_key_public.asc'
|
||||
)}`,
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['endpointRbacEnabled'])}`,
|
||||
`--xpack.fleet.enableExperimental=${JSON.stringify(['enableAutomaticAgentUpgrades'])}`,
|
||||
`--xpack.fleet.enableExperimental=${JSON.stringify([
|
||||
'enableAutomaticAgentUpgrades',
|
||||
'installedIntegrationsTabularUI',
|
||||
])}`,
|
||||
`--xpack.cloud.id='123456789'`,
|
||||
`--xpack.fleet.agentless.enabled=true`,
|
||||
`--xpack.fleet.agentless.api.url=https://api.agentless.url/api/v1/ess`,
|
||||
|
|
|
@ -149,6 +149,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
'fleet:check-deleted-files-task',
|
||||
'fleet:delete-unenrolled-agents-task',
|
||||
'fleet:deploy_agent_policies',
|
||||
'fleet:packages-bulk-operations',
|
||||
'fleet:reassign_action:retry',
|
||||
'fleet:request_diagnostics:retry',
|
||||
'fleet:setup:upgrade_managed_package_policies',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue