mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Fleet] Handle install status and errors when uninstalling remote integrations is enabled (#220990)
Closes https://github.com/elastic/kibana/issues/217683 ## Summary Follow up of https://github.com/elastic/kibana/pull/217144 Handle errors occurring when `sync_uninstalled_integrations` is enabled on remote outputs; these errors are now saved under `latest_uninstall_failed_attempts` and will be reported by `api/fleet/remote_synced_integrations/<output_id>/remote_status` and `api/fleet/remote_synced_integrations/remote_status`. - I added a new field in the response of these apis that allows to understand at a glance the install status of an integration on both cluster: ``` install_status: { main: 'installed', remote: 'not_installed', } ``` - Added a new "warning" state for synced integrations - Handled the case when an integration was successfully uninstalled from both clusters (marked as complete) - Removed the "throw error" for the case of `outputId` in favour of a regular error in the response ### Testing - Follow steps in https://github.com/elastic/kibana/pull/217144 - Check that the errors reported in `latest_uninstall_failed_attempts` are now visible when querying `api/fleet/remote_synced_integrations/<output_id>/remote_status` under the new "warning" field. This can be done also from the UI, checking the network tab - <img width="2111" alt="Screenshot 2025-05-20 at 11 17 13" src="https://github.com/user-attachments/assets/80a077e7-8b1b-4d04-abe9-0ef0cc44def8" /> Response for the failed uninstalled integration: ``` { "package_name": "akamai", "package_version": "2.28.0", "install_status": { "main": "not_installed", "remote": "installed" }, "updated_at": "2025-05-21T09:34:34.492Z", "sync_status": "warning", "warning": "Unable to remove package akamai:2.28.0 with existing package policy(s) in use by agent(s) at Fri, 23 May 2025 07:54:41 GMT" }, ``` ### UI changes The integrations uninstalled from the main cluster are now shown with a greyed out text and the warning is shown on screen as well: <img width="703" alt="Screenshot 2025-05-23 at 10 37 57" src="https://github.com/user-attachments/assets/a6900e0b-96cc-4bcc-8f16-db0001f55de3" /> ### Checklist - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
cde7a86287
commit
b7506d70af
22 changed files with 871 additions and 250 deletions
|
@ -44165,7 +44165,8 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44197,6 +44198,21 @@
|
|||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"install_status": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"main"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"package_name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44207,20 +44223,28 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sync_status"
|
||||
"sync_status",
|
||||
"install_status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -44312,7 +44336,8 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44344,6 +44369,21 @@
|
|||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"install_status": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"main"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"package_name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44354,20 +44394,28 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sync_status"
|
||||
"sync_status",
|
||||
"install_status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
|
@ -44165,7 +44165,8 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44197,6 +44198,21 @@
|
|||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"install_status": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"main"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"package_name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44207,20 +44223,28 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sync_status"
|
||||
"sync_status",
|
||||
"install_status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
@ -44312,7 +44336,8 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44344,6 +44369,21 @@
|
|||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"install_status": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"main": {
|
||||
"type": "string"
|
||||
},
|
||||
"remote": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"main"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"package_name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -44354,20 +44394,28 @@
|
|||
"enum": [
|
||||
"completed",
|
||||
"synchronizing",
|
||||
"failed"
|
||||
"failed",
|
||||
"warning"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"sync_status"
|
||||
"sync_status",
|
||||
"install_status"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"warning": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
|
@ -38994,6 +38994,7 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
|
@ -39015,6 +39016,16 @@ paths:
|
|||
type: string
|
||||
id:
|
||||
type: string
|
||||
install_status:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
main:
|
||||
type: string
|
||||
remote:
|
||||
type: string
|
||||
required:
|
||||
- main
|
||||
package_name:
|
||||
type: string
|
||||
package_version:
|
||||
|
@ -39024,12 +39035,18 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- sync_status
|
||||
- install_status
|
||||
type: array
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- integrations
|
||||
'400':
|
||||
|
@ -39088,6 +39105,7 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
|
@ -39109,6 +39127,16 @@ paths:
|
|||
type: string
|
||||
id:
|
||||
type: string
|
||||
install_status:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
main:
|
||||
type: string
|
||||
remote:
|
||||
type: string
|
||||
required:
|
||||
- main
|
||||
package_name:
|
||||
type: string
|
||||
package_version:
|
||||
|
@ -39118,12 +39146,18 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- sync_status
|
||||
- install_status
|
||||
type: array
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- integrations
|
||||
'400':
|
||||
|
|
|
@ -41236,6 +41236,7 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
|
@ -41257,6 +41258,16 @@ paths:
|
|||
type: string
|
||||
id:
|
||||
type: string
|
||||
install_status:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
main:
|
||||
type: string
|
||||
remote:
|
||||
type: string
|
||||
required:
|
||||
- main
|
||||
package_name:
|
||||
type: string
|
||||
package_version:
|
||||
|
@ -41266,12 +41277,18 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- sync_status
|
||||
- install_status
|
||||
type: array
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- integrations
|
||||
'400':
|
||||
|
@ -41330,6 +41347,7 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
|
@ -41351,6 +41369,16 @@ paths:
|
|||
type: string
|
||||
id:
|
||||
type: string
|
||||
install_status:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
main:
|
||||
type: string
|
||||
remote:
|
||||
type: string
|
||||
required:
|
||||
- main
|
||||
package_name:
|
||||
type: string
|
||||
package_version:
|
||||
|
@ -41360,12 +41388,18 @@ paths:
|
|||
- completed
|
||||
- synchronizing
|
||||
- failed
|
||||
- warning
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- sync_status
|
||||
- install_status
|
||||
type: array
|
||||
warning:
|
||||
type: string
|
||||
required:
|
||||
- integrations
|
||||
'400':
|
||||
|
|
|
@ -79,7 +79,6 @@ module.exports = {
|
|||
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]settings[\/\\]components[\/\\]fleet_server_hosts_table[\/\\]index.tsx/,
|
||||
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]settings[\/\\]components[\/\\]multi_row_input[\/\\]index.tsx/,
|
||||
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]settings[\/\\]components[\/\\]outputs_table[\/\\]index.tsx/,
|
||||
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]settings[\/\\]components[\/\\]outputs_table[\/\\]integration_status.tsx/,
|
||||
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]fleet[\/\\]sections[\/\\]settings[\/\\]components[\/\\]outputs_table[\/\\]integration_sync_flyout.test.tsx/,
|
||||
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]integrations[\/\\]sections[\/\\]epm[\/\\]components[\/\\]integration_preference.tsx/,
|
||||
/x-pack[\/\\]platform[\/\\]plugins[\/\\]shared[\/\\]fleet[\/\\]public[\/\\]applications[\/\\]integrations[\/\\]sections[\/\\]epm[\/\\]components[\/\\]package_list_grid[\/\\]controls.tsx/,
|
||||
|
|
|
@ -882,6 +882,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
|
|||
unprivilegedMode: `${ELASTIC_DOCS}reference/fleet/elastic-agent-unprivileged#unprivileged-change-mode`,
|
||||
httpMonitoring: `${ELASTIC_DOCS}reference/fleet/agent-policy#change-policy-enable-agent-monitoring`,
|
||||
agentLevelLogging: `${ELASTIC_DOCS}reference/fleet/monitor-elastic-agent#change-logging-level`,
|
||||
remoteESOoutputTroubleshooting: `${ELASTIC_DOCS}reference/fleet/remote-elasticsearch-output#troubleshooting`,
|
||||
},
|
||||
integrationDeveloper: {
|
||||
upload: `${ELASTIC_DOCS}extend/integrations/upload-new-integration`,
|
||||
|
|
|
@ -529,6 +529,7 @@ export interface DocLinks {
|
|||
unprivilegedMode: string;
|
||||
httpMonitoring: string;
|
||||
agentLevelLogging: string;
|
||||
remoteESOoutputTroubleshooting: string;
|
||||
}>;
|
||||
readonly integrationDeveloper: {
|
||||
upload: string;
|
||||
|
|
|
@ -10,6 +10,7 @@ export enum SyncStatus {
|
|||
SYNCHRONIZING = 'synchronizing',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed',
|
||||
WARNING = 'warning',
|
||||
}
|
||||
|
||||
export interface RemoteSyncedIntegrationsBase {
|
||||
|
@ -20,7 +21,12 @@ export interface RemoteSyncedIntegrationsBase {
|
|||
export interface RemoteSyncedIntegrationsStatus extends RemoteSyncedIntegrationsBase {
|
||||
sync_status: SyncStatus;
|
||||
error?: string;
|
||||
warning?: string;
|
||||
updated_at?: string;
|
||||
install_status: {
|
||||
main: string;
|
||||
remote?: string;
|
||||
};
|
||||
}
|
||||
export interface RemoteSyncedCustomAssetsStatus extends BaseCustomAssetsData {
|
||||
sync_status: SyncStatus;
|
||||
|
|
|
@ -10,4 +10,5 @@ export interface GetRemoteSyncedIntegrationsStatusResponse {
|
|||
integrations: RemoteSyncedIntegrationsStatus[];
|
||||
custom_assets?: RemoteSyncedCustomAssetsRecord;
|
||||
error?: string;
|
||||
warning?: string;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { memo, useEffect, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { css } from '@emotion/react';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
|
@ -19,6 +19,8 @@ import {
|
|||
EuiIcon,
|
||||
EuiLoadingSpinner,
|
||||
EuiBadge,
|
||||
useEuiTheme,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import type { EuiAccordionProps } from '@elastic/eui/src/components/accordion';
|
||||
|
@ -34,51 +36,12 @@ import type {
|
|||
import { SyncStatus } from '../../../../../../../common/types';
|
||||
import { PackageIcon } from '../../../../../../components';
|
||||
|
||||
import { sendGetPackageInfoByKeyForRq } from '../../../../hooks';
|
||||
import { sendGetPackageInfoByKeyForRq, useStartServices } from '../../../../hooks';
|
||||
|
||||
import { IntegrationStatusBadge } from './integration_status_badge';
|
||||
import { getIntegrationStatus } from './integration_sync_status';
|
||||
|
||||
const StyledEuiPanel = styled(EuiPanel)`
|
||||
border: solid 1px ${(props) => props.theme.eui.euiFormBorderColor};
|
||||
box-shadow: none;
|
||||
border-radius: 6px;
|
||||
`;
|
||||
|
||||
const StyledEuiAccordion = styled(EuiAccordion)`
|
||||
.euiAccordion__button {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.euiAccordion__triggerWrapper {
|
||||
padding-left: ${(props) => props.theme.eui.euiSizeM};
|
||||
}
|
||||
|
||||
&.euiAccordion-isOpen {
|
||||
.euiAccordion__childWrapper {
|
||||
padding: ${(props) => props.theme.eui.euiSizeM};
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ingest-integration-title-button {
|
||||
padding: ${(props) => props.theme.eui.euiSizeS};
|
||||
}
|
||||
|
||||
.euiTableRow:last-child .euiTableRowCell {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.euiIEFlexWrapFix {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.euiAccordion__buttonContent {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const CollapsablePanel: React.FC<{
|
||||
const CollapsiblePanel: React.FC<{
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
title: React.ReactNode;
|
||||
|
@ -92,10 +55,47 @@ const CollapsablePanel: React.FC<{
|
|||
}
|
||||
return undefined;
|
||||
}, [dataTestSubj]);
|
||||
|
||||
const { euiTheme } = useEuiTheme();
|
||||
return (
|
||||
<StyledEuiPanel paddingSize="none">
|
||||
<StyledEuiAccordion
|
||||
<EuiPanel
|
||||
paddingSize="none"
|
||||
css={css`
|
||||
border: solid 1px ${euiTheme.colors.borderBasePlain};
|
||||
box-shadow: none;
|
||||
border-radius: 6px;
|
||||
`}
|
||||
>
|
||||
<EuiAccordion
|
||||
css={css`
|
||||
.euiAccordion__button {
|
||||
width: 90%;
|
||||
}
|
||||
.euiAccordion__triggerWrapper {
|
||||
padding-left: ${euiTheme.size.m};
|
||||
}
|
||||
&.euiAccordion-isOpen {
|
||||
.euiAccordion__childWrapper {
|
||||
padding: ${euiTheme.size.m};
|
||||
padding-top: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ingest-integration-title-button {
|
||||
padding: ${euiTheme.size.s};
|
||||
}
|
||||
|
||||
.euiTableRow:last-child .euiTableRowCell {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.euiIEFlexWrapFix {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.euiAccordion__buttonContent {
|
||||
width: 100%;
|
||||
}
|
||||
`}
|
||||
id={id}
|
||||
arrowDisplay="left"
|
||||
buttonClassName="ingest-integration-title-button"
|
||||
|
@ -104,147 +104,212 @@ const CollapsablePanel: React.FC<{
|
|||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
{children}
|
||||
</StyledEuiAccordion>
|
||||
</StyledEuiPanel>
|
||||
</EuiAccordion>
|
||||
</EuiPanel>
|
||||
);
|
||||
};
|
||||
|
||||
export const IntegrationStatus: React.FunctionComponent<{
|
||||
integration: RemoteSyncedIntegrationsStatus;
|
||||
customAssets: RemoteSyncedCustomAssetsStatus[];
|
||||
syncUninstalledIntegrations?: boolean;
|
||||
'data-test-subj'?: string;
|
||||
}> = memo(({ integration, customAssets, 'data-test-subj': dataTestSubj }) => {
|
||||
const [packageInfo, setPackageInfo] = useState<PackageInfo | undefined>(undefined);
|
||||
}> = memo(
|
||||
({ integration, customAssets, syncUninstalledIntegrations, 'data-test-subj': dataTestSubj }) => {
|
||||
const [packageInfo, setPackageInfo] = useState<PackageInfo | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
sendGetPackageInfoByKeyForRq(integration.package_name, integration.package_version, {
|
||||
prerelease: true,
|
||||
}).then((result: GetInfoResponse) => {
|
||||
setPackageInfo(result.item);
|
||||
});
|
||||
}, [integration.package_name, integration.package_version]);
|
||||
useEffect(() => {
|
||||
sendGetPackageInfoByKeyForRq(integration.package_name, integration.package_version, {
|
||||
prerelease: true,
|
||||
}).then((result: GetInfoResponse) => {
|
||||
setPackageInfo(result.item);
|
||||
});
|
||||
}, [integration.package_name, integration.package_version]);
|
||||
|
||||
const statuses = [integration.sync_status, ...customAssets.map((asset) => asset.sync_status)];
|
||||
const integrationStatus = getIntegrationStatus(statuses).toUpperCase();
|
||||
const statuses = [integration.sync_status, ...customAssets.map((asset) => asset.sync_status)];
|
||||
const integrationStatus = getIntegrationStatus(statuses).toUpperCase();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
const { docLinks } = useStartServices();
|
||||
|
||||
return (
|
||||
<CollapsablePanel
|
||||
id={integration.package_name}
|
||||
data-test-subj={dataTestSubj}
|
||||
title={
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<PackageIcon
|
||||
packageName={integration.package_name}
|
||||
version={integration.package_version}
|
||||
size="l"
|
||||
tryApi={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<EuiTitle size="xs">
|
||||
<p>{packageInfo?.title ?? integration.package_name}</p>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<IntegrationStatusBadge status={integrationStatus} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{integration.error && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.integrationErrorTitle"
|
||||
defaultMessage="Error"
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
size="s"
|
||||
data-test-subj="integrationSyncIntegrationErrorCallout"
|
||||
>
|
||||
<EuiText size="s">{integration.error}</EuiText>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
{customAssets.map((customAsset) => {
|
||||
return (
|
||||
<EuiAccordion
|
||||
id={`${customAsset.type}:${customAsset.name}`}
|
||||
key={`${customAsset.type}:${customAsset.name}`}
|
||||
buttonContent={
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{customAsset.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
{customAsset.is_deleted && (
|
||||
const titleTextColor =
|
||||
integration.install_status.main !== 'installed'
|
||||
? euiTheme.colors.textDisabled
|
||||
: euiTheme.colors.textParagraph;
|
||||
|
||||
return (
|
||||
<CollapsiblePanel
|
||||
id={integration.package_name}
|
||||
data-test-subj={dataTestSubj}
|
||||
title={
|
||||
<EuiTitle size="xs">
|
||||
<h3>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center" justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.deletedText"
|
||||
defaultMessage="Deleted"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
data-test-subj={`${customAsset.type}:${customAsset.name}-accordion`}
|
||||
extraAction={
|
||||
customAsset.sync_status === SyncStatus.SYNCHRONIZING ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
<EuiIcon
|
||||
size="m"
|
||||
color={customAsset.sync_status === SyncStatus.FAILED ? 'danger' : 'success'}
|
||||
type={
|
||||
customAsset.sync_status === SyncStatus.FAILED
|
||||
? 'errorFilled'
|
||||
: 'checkInCircleFilled'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
paddingSize="none"
|
||||
>
|
||||
{customAsset.error && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.errorTitle"
|
||||
defaultMessage="Error"
|
||||
<PackageIcon
|
||||
packageName={integration.package_name}
|
||||
version={integration.package_version}
|
||||
size="l"
|
||||
tryApi={true}
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
size="s"
|
||||
data-test-subj="integrationSyncAssetErrorCallout"
|
||||
>
|
||||
<EuiText size="s">{customAsset.error}</EuiText>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="m" />
|
||||
</>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</CollapsablePanel>
|
||||
);
|
||||
});
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem className="eui-textTruncate">
|
||||
<EuiTitle
|
||||
size="xs"
|
||||
css={css`
|
||||
color: ${titleTextColor};
|
||||
`}
|
||||
>
|
||||
<p>{packageInfo?.title ?? integration.package_name}</p>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<IntegrationStatusBadge status={integrationStatus} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
}
|
||||
>
|
||||
<>
|
||||
{integration?.error && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.integrationErrorTitle"
|
||||
defaultMessage="Error"
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
size="s"
|
||||
data-test-subj="integrationSyncIntegrationErrorCallout"
|
||||
>
|
||||
<EuiText size="s">{integration.error}</EuiText>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{integration.sync_status === 'warning' && (
|
||||
<>
|
||||
<EuiCallOut
|
||||
title={
|
||||
syncUninstalledIntegrations ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.integrationWarningContent"
|
||||
defaultMessage="Integration was uninstalled, but removal from remote cluster failed."
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.integrationErrorTitle"
|
||||
defaultMessage="Warning"
|
||||
/>
|
||||
)
|
||||
}
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
size="s"
|
||||
data-test-subj="integrationSyncIntegrationWarningCallout"
|
||||
>
|
||||
{integration?.warning && (
|
||||
<EuiText size="s">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.integrationWarningContent"
|
||||
defaultMessage="{uninstallWarning}"
|
||||
values={{
|
||||
uninstallWarning: integration?.warning,
|
||||
}}
|
||||
/>
|
||||
</EuiText>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<EuiButton
|
||||
color="warning"
|
||||
href={docLinks.links.fleet.remoteESOoutputTroubleshooting}
|
||||
iconType="popout"
|
||||
target="blank"
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.integrationWarningButton"
|
||||
defaultMessage="View troubleshooting guide"
|
||||
/>
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{customAssets.map((customAsset) => {
|
||||
return (
|
||||
<EuiAccordion
|
||||
id={`${customAsset.type}:${customAsset.name}`}
|
||||
key={`${customAsset.type}:${customAsset.name}`}
|
||||
buttonContent={
|
||||
<EuiFlexGroup alignItems="baseline" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText size="s">{customAsset.name}</EuiText>
|
||||
</EuiFlexItem>
|
||||
{customAsset.is_deleted && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.deletedText"
|
||||
defaultMessage="Deleted"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
data-test-subj={`${customAsset.type}:${customAsset.name}-accordion`}
|
||||
extraAction={
|
||||
customAsset.sync_status === SyncStatus.SYNCHRONIZING ? (
|
||||
<EuiLoadingSpinner size="m" />
|
||||
) : (
|
||||
<EuiIcon
|
||||
size="m"
|
||||
color={customAsset.sync_status === SyncStatus.FAILED ? 'danger' : 'success'}
|
||||
type={
|
||||
customAsset.sync_status === SyncStatus.FAILED
|
||||
? 'errorFilled'
|
||||
: 'checkInCircleFilled'
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
paddingSize="none"
|
||||
>
|
||||
{customAsset.error && (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiCallOut
|
||||
title={
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.errorTitle"
|
||||
defaultMessage="Error"
|
||||
/>
|
||||
}
|
||||
color="danger"
|
||||
iconType="error"
|
||||
size="s"
|
||||
data-test-subj="integrationSyncAssetErrorCallout"
|
||||
>
|
||||
<EuiText size="s">{customAsset.error}</EuiText>
|
||||
</EuiCallOut>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
)}
|
||||
</EuiAccordion>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</CollapsiblePanel>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -45,6 +45,19 @@ export const IntegrationStatusBadge: React.FunctionComponent<{
|
|||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
WARNING: (
|
||||
<EuiBadge
|
||||
color="warning"
|
||||
data-test-subj="integrationSyncWarningBadge"
|
||||
iconType="warning"
|
||||
{...onClickProps}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrationSyncStatus.warningText"
|
||||
defaultMessage="Warning"
|
||||
/>
|
||||
</EuiBadge>
|
||||
),
|
||||
COMPLETED: (
|
||||
<EuiBadge
|
||||
color="success"
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('IntegrationSyncFlyout', () => {
|
|||
package_name: 'elastic_agent',
|
||||
package_version: '2.2.1',
|
||||
sync_status: SyncStatus.COMPLETED,
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: '2025-04-14T11:53:00.925Z',
|
||||
},
|
||||
{
|
||||
|
@ -44,14 +45,37 @@ describe('IntegrationSyncFlyout', () => {
|
|||
package_version: '1.25.1',
|
||||
sync_status: SyncStatus.FAILED,
|
||||
updated_at: '2025-04-14T11:53:00.925Z',
|
||||
install_status: { main: 'installed', remote: 'not_installed' },
|
||||
error: 'Nginx failed to install',
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.68.0',
|
||||
sync_status: SyncStatus.SYNCHRONIZING,
|
||||
install_status: { main: 'installed', remote: 'not_installed' },
|
||||
updated_at: '2025-04-14T11:53:04.106Z',
|
||||
},
|
||||
{
|
||||
package_name: '1password',
|
||||
package_version: '1.32.0',
|
||||
install_status: {
|
||||
main: 'not_installed',
|
||||
remote: 'installed',
|
||||
},
|
||||
updated_at: '2025-05-19T15:40:26.554Z',
|
||||
sync_status: SyncStatus.WARNING,
|
||||
warning: 'Unable to remove package 1password:1.32.0',
|
||||
},
|
||||
{
|
||||
package_name: 'apache',
|
||||
package_version: '1.0.0',
|
||||
install_status: {
|
||||
main: 'not_installed',
|
||||
remote: 'not_installed',
|
||||
},
|
||||
updated_at: '2025-05-19T15:40:26.554Z',
|
||||
sync_status: SyncStatus.COMPLETED,
|
||||
},
|
||||
],
|
||||
custom_assets: {
|
||||
'component_template:logs-system.auth@custom': {
|
||||
|
@ -127,6 +151,8 @@ describe('IntegrationSyncFlyout', () => {
|
|||
expect(component.getByTestId('elastic_agent-accordion').textContent).toContain('Completed');
|
||||
expect(component.getByTestId('nginx-accordion').textContent).toContain('Failed');
|
||||
expect(component.getByTestId('system-accordion').textContent).toContain('Syncing...');
|
||||
expect(component.getByTestId('1password-accordion').textContent).toContain('Warning');
|
||||
expect(component.queryByTestId('apache-accordion')).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(component.getByTestId('nginx-accordion-openCloseToggle'));
|
||||
expect(component.getByTestId('integrationSyncIntegrationErrorCallout').textContent).toEqual(
|
||||
|
|
|
@ -31,12 +31,13 @@ import { IntegrationStatus } from './integration_status';
|
|||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
syncedIntegrationsStatus?: GetRemoteSyncedIntegrationsStatusResponse;
|
||||
outputName: string;
|
||||
syncedIntegrationsStatus?: GetRemoteSyncedIntegrationsStatusResponse;
|
||||
syncUninstalledIntegrations?: boolean;
|
||||
}
|
||||
|
||||
export const IntegrationSyncFlyout: React.FunctionComponent<Props> = memo(
|
||||
({ onClose, syncedIntegrationsStatus, outputName }) => {
|
||||
({ onClose, syncedIntegrationsStatus, outputName, syncUninstalledIntegrations }) => {
|
||||
const { docLinks } = useStartServices();
|
||||
return (
|
||||
<EuiFlyout onClose={onClose}>
|
||||
|
@ -89,20 +90,30 @@ export const IntegrationSyncFlyout: React.FunctionComponent<Props> = memo(
|
|||
</EuiCallOut>
|
||||
)}
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
{(syncedIntegrationsStatus?.integrations ?? []).map((integration) => {
|
||||
const customAssets = Object.values(
|
||||
syncedIntegrationsStatus?.custom_assets ?? {}
|
||||
).filter((asset) => asset.package_name === integration.package_name);
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={integration.package_name}>
|
||||
<IntegrationStatus
|
||||
data-test-subj={`${integration.package_name}-accordion`}
|
||||
integration={integration}
|
||||
customAssets={customAssets}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
{(syncedIntegrationsStatus?.integrations ?? [])
|
||||
// don't show integrations that were successfully uninstalled
|
||||
.filter(
|
||||
(integration) =>
|
||||
!(
|
||||
integration.install_status?.main === 'not_installed' &&
|
||||
integration.install_status?.remote === 'not_installed'
|
||||
)
|
||||
)
|
||||
.map((integration) => {
|
||||
const customAssets = Object.values(
|
||||
syncedIntegrationsStatus?.custom_assets ?? {}
|
||||
).filter((asset) => asset.package_name === integration.package_name);
|
||||
return (
|
||||
<EuiFlexItem grow={false} key={integration.package_name}>
|
||||
<IntegrationStatus
|
||||
data-test-subj={`${integration.package_name}-accordion`}
|
||||
integration={integration}
|
||||
customAssets={customAssets}
|
||||
syncUninstalledIntegrations={syncUninstalledIntegrations}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
|
|
|
@ -21,6 +21,8 @@ interface Props {
|
|||
export function getIntegrationStatus(statuses: SyncStatus[]): SyncStatus {
|
||||
return statuses.some((current) => current === SyncStatus.FAILED)
|
||||
? SyncStatus.FAILED
|
||||
: statuses.some((current) => current === SyncStatus.WARNING)
|
||||
? SyncStatus.WARNING
|
||||
: statuses.some((current) => current === SyncStatus.SYNCHRONIZING)
|
||||
? SyncStatus.SYNCHRONIZING
|
||||
: SyncStatus.COMPLETED;
|
||||
|
@ -44,10 +46,15 @@ export const IntegrationSyncStatus: React.FunctionComponent<Props> = memo(({ out
|
|||
if (!error && !syncedIntegrationsStatus) {
|
||||
return 'SYNCHRONIZING';
|
||||
}
|
||||
|
||||
const installedSyncedIntegrations = (syncedIntegrationsStatus?.integrations ?? []).filter(
|
||||
(integration) =>
|
||||
!(
|
||||
integration.install_status?.main === 'not_installed' &&
|
||||
integration.install_status?.remote === 'not_installed'
|
||||
)
|
||||
);
|
||||
const statuses = [
|
||||
...(syncedIntegrationsStatus?.integrations?.map((integration) => integration.sync_status) ||
|
||||
[]),
|
||||
...(installedSyncedIntegrations.map((integration) => integration.sync_status) || []),
|
||||
...Object.values(syncedIntegrationsStatus?.custom_assets ?? {}).map(
|
||||
(asset) => asset.sync_status
|
||||
),
|
||||
|
@ -85,6 +92,9 @@ export const IntegrationSyncStatus: React.FunctionComponent<Props> = memo(({ out
|
|||
: syncedIntegrationsStatus
|
||||
}
|
||||
outputName={output.name}
|
||||
syncUninstalledIntegrations={
|
||||
output.type === 'remote_elasticsearch' && output?.sync_uninstalled_integrations
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -257,14 +257,23 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
sync_status: 'synchronizing',
|
||||
package_name: 'elastic_agent',
|
||||
package_version: '2.2.0',
|
||||
install_status: { main: 'installed' },
|
||||
updated_at: '2025-03-20T14:18:40.076Z',
|
||||
},
|
||||
{
|
||||
sync_status: 'synchronizing',
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
install_status: { main: 'installed' },
|
||||
updated_at: '2025-03-20T14:18:40.111Z',
|
||||
},
|
||||
{
|
||||
package_name: 'fleet_server',
|
||||
package_version: '1.67.3',
|
||||
updated_at: '2025-03-20T14:18:40.111Z',
|
||||
sync_status: 'synchronizing',
|
||||
install_status: { main: 'not_installed' },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -332,12 +341,205 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
package_name: 'elastic_agent',
|
||||
package_version: '2.2.0',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a warning when integration failed to uninstall if is_sync_uninstall_enabled', async () => {
|
||||
esClientMock.search.mockResolvedValueOnce({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
integrations: [
|
||||
{
|
||||
package_name: 'elastic_agent',
|
||||
package_version: '2.2.0',
|
||||
updated_at: '2025-03-20T14:18:40.076Z',
|
||||
install_status: 'installed',
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
updated_at: '2025-03-20T14:18:40.111Z',
|
||||
install_status: 'installed',
|
||||
},
|
||||
{
|
||||
package_name: 'nginx',
|
||||
package_version: '0.7.0',
|
||||
updated_at: '2025-03-20T14:18:40.111Z',
|
||||
install_status: 'not_installed',
|
||||
},
|
||||
],
|
||||
remote_es_hosts: [
|
||||
{
|
||||
name: 'remote1',
|
||||
hosts: ['http://localhost:9500'],
|
||||
sync_integrations: true,
|
||||
sync_uninstalled_integrations: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
(getPackageSavedObjects as jest.MockedFunction<any>).mockReturnValue({
|
||||
page: 1,
|
||||
per_page: 10000,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'epm-packages',
|
||||
id: 'elastic_agent',
|
||||
attributes: {
|
||||
version: '2.2.0',
|
||||
install_status: 'installed',
|
||||
},
|
||||
updated_at: '2025-03-26T14:06:27.611Z',
|
||||
},
|
||||
{
|
||||
type: 'epm-packages',
|
||||
id: 'system',
|
||||
attributes: {
|
||||
version: '1.67.3',
|
||||
install_status: 'installed',
|
||||
},
|
||||
updated_at: '2025-03-26T14:06:27.611Z',
|
||||
},
|
||||
{
|
||||
type: 'epm-packages',
|
||||
id: 'nginx',
|
||||
attributes: {
|
||||
version: '0.7.0',
|
||||
install_status: 'installed',
|
||||
updated_at: '2025-03-26T14:06:27.611Z',
|
||||
latest_uninstall_failed_attempts: [
|
||||
{
|
||||
created_at: '2025-03-26T14:06:27.611Z',
|
||||
error: {
|
||||
message: 'failed to uninstall',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const res = await fetchAndCompareSyncedIntegrations(
|
||||
esClientMock,
|
||||
soClientMock,
|
||||
'fleet-synced-integrations-ccr-*',
|
||||
mockedLogger
|
||||
);
|
||||
expect(res).toEqual({
|
||||
integrations: [
|
||||
{
|
||||
package_name: 'elastic_agent',
|
||||
package_version: '2.2.0',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
{
|
||||
warning: 'failed to uninstall at Wed, 26 Mar 2025 14:06:27 GMT',
|
||||
install_status: {
|
||||
main: 'not_installed',
|
||||
remote: 'installed',
|
||||
},
|
||||
package_name: 'nginx',
|
||||
package_version: '0.7.0',
|
||||
sync_status: 'warning',
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
it('should return status = completed when integration was uninstalled from both clusters if is_sync_uninstall_enabled', async () => {
|
||||
esClientMock.search.mockResolvedValueOnce({
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
integrations: [
|
||||
{
|
||||
package_name: 'nginx',
|
||||
package_version: '0.7.0',
|
||||
updated_at: '2025-03-20T14:18:40.111Z',
|
||||
install_status: 'not_installed',
|
||||
},
|
||||
],
|
||||
remote_es_hosts: [
|
||||
{
|
||||
name: 'remote1',
|
||||
hosts: ['http://localhost:9500'],
|
||||
sync_integrations: true,
|
||||
sync_uninstalled_integrations: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as any);
|
||||
(getPackageSavedObjects as jest.MockedFunction<any>).mockReturnValue({
|
||||
page: 1,
|
||||
per_page: 10000,
|
||||
total: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
type: 'epm-packages',
|
||||
id: 'elastic_agent',
|
||||
attributes: {
|
||||
version: '2.2.0',
|
||||
install_status: 'installed',
|
||||
},
|
||||
updated_at: '2025-03-26T14:06:27.611Z',
|
||||
},
|
||||
{
|
||||
type: 'epm-packages',
|
||||
id: 'system',
|
||||
attributes: {
|
||||
version: '1.67.3',
|
||||
install_status: 'installed',
|
||||
},
|
||||
updated_at: '2025-03-26T14:06:27.611Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
const res = await fetchAndCompareSyncedIntegrations(
|
||||
esClientMock,
|
||||
soClientMock,
|
||||
'fleet-synced-integrations-ccr-*',
|
||||
mockedLogger
|
||||
);
|
||||
expect(res).toEqual({
|
||||
integrations: [
|
||||
{
|
||||
install_status: {
|
||||
main: 'not_installed',
|
||||
remote: 'not_installed',
|
||||
},
|
||||
package_name: 'nginx',
|
||||
package_version: '0.7.0',
|
||||
sync_status: 'completed',
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
|
@ -394,7 +596,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
id: 'system',
|
||||
attributes: {
|
||||
version: '1.67.2',
|
||||
install_status: 'install',
|
||||
install_status: 'installed',
|
||||
},
|
||||
updated_at: '2025-03-26T14:06:27.611Z',
|
||||
},
|
||||
|
@ -428,20 +630,23 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
{
|
||||
package_name: 'elastic_agent',
|
||||
package_version: '2.2.0',
|
||||
install_status: { main: 'installed' },
|
||||
sync_status: 'synchronizing',
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
sync_status: 'failed',
|
||||
updated_at: expect.any(String),
|
||||
error: 'Found incorrect installed version 1.67.2',
|
||||
},
|
||||
{
|
||||
error: `Installation status: install_failed - reason: installation failure at Tue, 20 Jun 2023 08:47:31 GMT`,
|
||||
error: `Installation status: install_failed error: installation failure at Tue, 20 Jun 2023 08:47:31 GMT`,
|
||||
package_name: 'synthetics',
|
||||
package_version: '1.4.1',
|
||||
install_status: { main: 'installed', remote: 'install_failed' },
|
||||
sync_status: 'failed',
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
|
@ -589,6 +794,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
|
@ -700,6 +906,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
|
@ -803,6 +1010,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
sync_status: 'completed',
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
|
@ -948,6 +1156,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
|
@ -1026,6 +1235,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
sync_status: 'completed',
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
|
@ -1109,6 +1319,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
{
|
||||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
sync_status: 'completed',
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
|
@ -1226,6 +1437,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
|
@ -1315,6 +1527,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
|
@ -1395,6 +1608,7 @@ describe('fetchAndCompareSyncedIntegrations', () => {
|
|||
package_name: 'system',
|
||||
package_version: '1.67.3',
|
||||
sync_status: 'completed',
|
||||
install_status: { main: 'installed', remote: 'installed' },
|
||||
updated_at: expect.any(String),
|
||||
},
|
||||
],
|
||||
|
|
|
@ -102,10 +102,11 @@ export const fetchAndCompareSyncedIntegrations = async (
|
|||
};
|
||||
}
|
||||
const ccrIndex = searchRes.hits.hits[0]?._source;
|
||||
const { integrations: ccrIntegrations, custom_assets: ccrCustomAssets } = ccrIndex;
|
||||
const installedCCRIntegrations = ccrIntegrations?.filter(
|
||||
(integration) => integration.install_status !== 'not_installed'
|
||||
);
|
||||
const {
|
||||
integrations: ccrIntegrations,
|
||||
custom_assets: ccrCustomAssets,
|
||||
remote_es_hosts: remoteEsHosts,
|
||||
} = ccrIndex;
|
||||
|
||||
// find integrations installed on remote
|
||||
const installedIntegrations = await getPackageSavedObjects(savedObjectsClient);
|
||||
|
@ -125,9 +126,13 @@ export const fetchAndCompareSyncedIntegrations = async (
|
|||
ccrCustomAssets,
|
||||
installedIntegrationsByName
|
||||
);
|
||||
const isSyncUninstalledEnabled = remoteEsHosts?.some(
|
||||
(host) => host.sync_uninstalled_integrations
|
||||
);
|
||||
const integrationsStatus = compareIntegrations(
|
||||
installedCCRIntegrations,
|
||||
installedIntegrationsByName
|
||||
ccrIntegrations,
|
||||
installedIntegrationsByName,
|
||||
isSyncUninstalledEnabled
|
||||
);
|
||||
const result = {
|
||||
...integrationsStatus,
|
||||
|
@ -146,50 +151,122 @@ export const fetchAndCompareSyncedIntegrations = async (
|
|||
|
||||
const compareIntegrations = (
|
||||
ccrIntegrations: IntegrationsData[],
|
||||
installedIntegrationsByName: Record<string, SavedObjectsFindResult<Installation>>
|
||||
installedIntegrationsByName: Record<string, SavedObjectsFindResult<Installation>>,
|
||||
isSyncUninstalledEnabled: boolean
|
||||
): { integrations: RemoteSyncedIntegrationsStatus[] } => {
|
||||
const integrationsStatus: RemoteSyncedIntegrationsStatus[] | undefined = ccrIntegrations?.map(
|
||||
(ccrIntegration) => {
|
||||
const baseIntegrationData = {
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
install_status: ccrIntegration.install_status,
|
||||
};
|
||||
const localIntegrationSO = installedIntegrationsByName[ccrIntegration.package_name];
|
||||
if (!localIntegrationSO) {
|
||||
// Handle case of integration uninstalled from both clusters
|
||||
if (
|
||||
isSyncUninstalledEnabled &&
|
||||
!localIntegrationSO?.attributes &&
|
||||
ccrIntegration.install_status === 'not_installed'
|
||||
) {
|
||||
return {
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: SyncStatus.SYNCHRONIZING,
|
||||
...baseIntegrationData,
|
||||
install_status: {
|
||||
main: 'not_installed',
|
||||
remote: 'not_installed',
|
||||
},
|
||||
sync_status: SyncStatus.COMPLETED,
|
||||
updated_at: ccrIntegration?.updated_at,
|
||||
};
|
||||
}
|
||||
if (ccrIntegration.package_version !== localIntegrationSO?.attributes.version) {
|
||||
if (!localIntegrationSO) {
|
||||
return {
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
...baseIntegrationData,
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: SyncStatus.SYNCHRONIZING,
|
||||
install_status: { main: ccrIntegration.install_status },
|
||||
};
|
||||
}
|
||||
if (
|
||||
ccrIntegration.install_status !== 'not_installed' &&
|
||||
ccrIntegration.package_version !== localIntegrationSO?.attributes.version
|
||||
) {
|
||||
return {
|
||||
...baseIntegrationData,
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: SyncStatus.FAILED,
|
||||
install_status: {
|
||||
main: ccrIntegration.install_status,
|
||||
remote: localIntegrationSO?.attributes.install_status,
|
||||
},
|
||||
error: `Found incorrect installed version ${localIntegrationSO?.attributes.version}`,
|
||||
};
|
||||
}
|
||||
if (localIntegrationSO?.attributes.install_status === 'install_failed') {
|
||||
const latestFailedAttemptTime = localIntegrationSO?.attributes
|
||||
?.latest_install_failed_attempts?.[0].created_at
|
||||
? `at ${new Date(
|
||||
localIntegrationSO?.attributes?.latest_install_failed_attempts?.[0].created_at
|
||||
).toUTCString()}`
|
||||
: '';
|
||||
const latestFailedAttempt = localIntegrationSO?.attributes
|
||||
?.latest_install_failed_attempts?.[0]?.error?.message
|
||||
? `- reason: ${localIntegrationSO?.attributes?.latest_install_failed_attempts[0].error.message}`
|
||||
: '';
|
||||
if (
|
||||
ccrIntegration.install_status !== 'not_installed' &&
|
||||
localIntegrationSO?.attributes.install_status === 'install_failed'
|
||||
) {
|
||||
let latestFailedAttemptTime = '';
|
||||
let latestFailedAttempt = '';
|
||||
|
||||
if (localIntegrationSO?.attributes?.latest_install_failed_attempts?.[0]) {
|
||||
const latestInstallFailedAttempts =
|
||||
localIntegrationSO.attributes.latest_install_failed_attempts[0];
|
||||
latestFailedAttemptTime = `at ${new Date(
|
||||
latestInstallFailedAttempts.created_at
|
||||
).toUTCString()}`;
|
||||
latestFailedAttempt = latestInstallFailedAttempts.error?.message
|
||||
? `error: ${latestInstallFailedAttempts.error?.message}`
|
||||
: '';
|
||||
}
|
||||
|
||||
return {
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
...baseIntegrationData,
|
||||
install_status: {
|
||||
main: ccrIntegration.install_status,
|
||||
remote: localIntegrationSO?.attributes.install_status,
|
||||
},
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: SyncStatus.FAILED,
|
||||
error: `Installation status: ${localIntegrationSO?.attributes.install_status} ${latestFailedAttempt} ${latestFailedAttemptTime}`,
|
||||
};
|
||||
}
|
||||
if (
|
||||
isSyncUninstalledEnabled &&
|
||||
ccrIntegration.install_status === 'not_installed' &&
|
||||
localIntegrationSO?.attributes.install_status === 'installed'
|
||||
) {
|
||||
let latestUninstallFailedAttemptTime = '';
|
||||
let latestUninstallFailedAttempt = '';
|
||||
|
||||
if (localIntegrationSO?.attributes?.latest_uninstall_failed_attempts?.[0]) {
|
||||
const latestInstallFailedAttempts =
|
||||
localIntegrationSO.attributes.latest_uninstall_failed_attempts[0];
|
||||
latestUninstallFailedAttemptTime = `at ${new Date(
|
||||
latestInstallFailedAttempts.created_at
|
||||
).toUTCString()}`;
|
||||
latestUninstallFailedAttempt = latestInstallFailedAttempts.error?.message
|
||||
? `${latestInstallFailedAttempts.error?.message}`
|
||||
: '';
|
||||
}
|
||||
return {
|
||||
...baseIntegrationData,
|
||||
install_status: {
|
||||
main: ccrIntegration.install_status,
|
||||
remote: localIntegrationSO?.attributes.install_status,
|
||||
},
|
||||
updated_at: ccrIntegration.updated_at,
|
||||
sync_status: SyncStatus.WARNING,
|
||||
...(localIntegrationSO?.attributes.latest_uninstall_failed_attempts !== undefined
|
||||
? { warning: `${latestUninstallFailedAttempt} ${latestUninstallFailedAttemptTime}` }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
return {
|
||||
package_name: ccrIntegration.package_name,
|
||||
package_version: ccrIntegration.package_version,
|
||||
...baseIntegrationData,
|
||||
install_status: {
|
||||
main: ccrIntegration.install_status,
|
||||
remote: localIntegrationSO?.attributes.install_status,
|
||||
},
|
||||
sync_status:
|
||||
localIntegrationSO?.attributes.install_status === 'installed'
|
||||
? SyncStatus.COMPLETED
|
||||
|
|
|
@ -82,15 +82,15 @@ describe('getRemoteSyncedIntegrationsInfoByOutputId', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should throw error if the passed outputId is not found', async () => {
|
||||
it('should return response with error if the passed outputId is not found', async () => {
|
||||
jest
|
||||
.spyOn(mockedAppContextService, 'getExperimentalFeatures')
|
||||
.mockReturnValue({ enableSyncIntegrationsOnRemote: true } as any);
|
||||
mockedOutputService.get.mockRejectedValue({ isBoom: true, output: { statusCode: 404 } } as any);
|
||||
|
||||
await expect(
|
||||
getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote-es-not-existent')
|
||||
).rejects.toThrowError('No output found with id remote-es-not-existent');
|
||||
expect(
|
||||
await getRemoteSyncedIntegrationsInfoByOutputId(soClientMock, 'remote-es-not-existent')
|
||||
).toEqual({ error: 'No output found with id remote-es-not-existent', integrations: [] });
|
||||
});
|
||||
|
||||
it('should throw error if the passed outputId is not of type remote_elasticsearch', async () => {
|
||||
|
|
|
@ -82,7 +82,10 @@ export const getRemoteSyncedIntegrationsInfoByOutputId = async (
|
|||
};
|
||||
} catch (error) {
|
||||
if (error.isBoom && error.output.statusCode === 404) {
|
||||
throw new FleetNotFoundError(`No output found with id ${outputId}`);
|
||||
return {
|
||||
integrations: [],
|
||||
error: `No output found with id ${outputId}`,
|
||||
};
|
||||
} else if (error.type === 'system' && error.code === 'ECONNREFUSED') {
|
||||
throw new FleetError(`${error.message}${error.code}`);
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ export interface SyncIntegrationsData {
|
|||
name: string;
|
||||
hosts: string[];
|
||||
sync_integrations: boolean;
|
||||
sync_uninstalled_integrations?: boolean;
|
||||
}>;
|
||||
integrations: IntegrationsData[];
|
||||
custom_assets: {
|
||||
|
|
|
@ -224,8 +224,18 @@ describe('SyncIntegrationsTask', () => {
|
|||
},
|
||||
],
|
||||
remote_es_hosts: [
|
||||
{ hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: true },
|
||||
{ hosts: ['https://remote2:9200'], name: 'remote2', sync_integrations: false },
|
||||
{
|
||||
hosts: ['https://remote1:9200'],
|
||||
name: 'remote1',
|
||||
sync_integrations: true,
|
||||
sync_uninstalled_integrations: false,
|
||||
},
|
||||
{
|
||||
hosts: ['https://remote2:9200'],
|
||||
name: 'remote2',
|
||||
sync_integrations: false,
|
||||
sync_uninstalled_integrations: false,
|
||||
},
|
||||
],
|
||||
custom_assets: {
|
||||
'component_template:logs-system.auth@custom': {
|
||||
|
@ -297,7 +307,12 @@ describe('SyncIntegrationsTask', () => {
|
|||
},
|
||||
],
|
||||
remote_es_hosts: [
|
||||
{ hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: true },
|
||||
{
|
||||
hosts: ['https://remote1:9200'],
|
||||
name: 'remote1',
|
||||
sync_integrations: true,
|
||||
sync_uninstalled_integrations: false,
|
||||
},
|
||||
],
|
||||
custom_assets: {},
|
||||
custom_assets_error: {
|
||||
|
@ -461,7 +476,12 @@ describe('SyncIntegrationsTask', () => {
|
|||
},
|
||||
],
|
||||
remote_es_hosts: [
|
||||
{ hosts: ['https://remote1:9200'], name: 'remote1', sync_integrations: true },
|
||||
{
|
||||
hosts: ['https://remote1:9200'],
|
||||
name: 'remote1',
|
||||
sync_integrations: true,
|
||||
sync_uninstalled_integrations: true,
|
||||
},
|
||||
],
|
||||
custom_assets: {
|
||||
'component_template:logs-system.auth@custom': {
|
||||
|
|
|
@ -225,6 +225,7 @@ export class SyncIntegrationsTask {
|
|||
name: remoteOutput.name,
|
||||
hosts: remoteOutput.hosts ?? [],
|
||||
sync_integrations: remoteOutput.sync_integrations ?? false,
|
||||
sync_uninstalled_integrations: remoteOutput.sync_uninstalled_integrations ?? false,
|
||||
};
|
||||
}),
|
||||
integrations: [],
|
||||
|
|
|
@ -20,9 +20,15 @@ export const RemoteSyncedIntegrationsStatusSchema = RemoteSyncedIntegrationsBase
|
|||
schema.literal(SyncStatus.COMPLETED),
|
||||
schema.literal(SyncStatus.SYNCHRONIZING),
|
||||
schema.literal(SyncStatus.FAILED),
|
||||
schema.literal(SyncStatus.WARNING),
|
||||
]),
|
||||
error: schema.maybe(schema.string()),
|
||||
warning: schema.maybe(schema.string()),
|
||||
updated_at: schema.maybe(schema.string()),
|
||||
install_status: schema.object({
|
||||
main: schema.string(),
|
||||
remote: schema.maybe(schema.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const CustomAssetsDataSchema = schema.object({
|
||||
|
@ -34,6 +40,7 @@ export const CustomAssetsDataSchema = schema.object({
|
|||
schema.literal(SyncStatus.COMPLETED),
|
||||
schema.literal(SyncStatus.SYNCHRONIZING),
|
||||
schema.literal(SyncStatus.FAILED),
|
||||
schema.literal(SyncStatus.WARNING),
|
||||
]),
|
||||
error: schema.maybe(schema.string()),
|
||||
is_deleted: schema.maybe(schema.boolean()),
|
||||
|
@ -43,4 +50,5 @@ export const GetRemoteSyncedIntegrationsStatusResponseSchema = schema.object({
|
|||
integrations: schema.arrayOf(RemoteSyncedIntegrationsStatusSchema),
|
||||
custom_assets: schema.maybe(schema.recordOf(schema.string(), CustomAssetsDataSchema)),
|
||||
error: schema.maybe(schema.string()),
|
||||
warning: schema.maybe(schema.string()),
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue