[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:
Cristina Amico 2025-05-28 18:24:00 +02:00 committed by GitHub
parent cde7a86287
commit b7506d70af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 871 additions and 250 deletions

View file

@ -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": [

View file

@ -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": [

View file

@ -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':

View file

@ -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':

View file

@ -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/,

View file

@ -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`,

View file

@ -529,6 +529,7 @@ export interface DocLinks {
unprivilegedMode: string;
httpMonitoring: string;
agentLevelLogging: string;
remoteESOoutputTroubleshooting: string;
}>;
readonly integrationDeveloper: {
upload: string;

View file

@ -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;

View file

@ -10,4 +10,5 @@ export interface GetRemoteSyncedIntegrationsStatusResponse {
integrations: RemoteSyncedIntegrationsStatus[];
custom_assets?: RemoteSyncedCustomAssetsRecord;
error?: string;
warning?: string;
}

View file

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

View file

@ -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"

View file

@ -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(

View file

@ -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>

View file

@ -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
}
/>
)}
</>

View file

@ -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),
},
],

View file

@ -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

View file

@ -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 () => {

View file

@ -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}`);
}

View file

@ -34,6 +34,7 @@ export interface SyncIntegrationsData {
name: string;
hosts: string[];
sync_integrations: boolean;
sync_uninstalled_integrations?: boolean;
}>;
integrations: IntegrationsData[];
custom_assets: {

View file

@ -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': {

View file

@ -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: [],

View file

@ -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()),
});