[Fleet] enable feature flag enableSyncIntegrationsOnRemote (#220215)

## Summary

Closes https://github.com/elastic/kibana/issues/217490

- Enable feature flag `enableSyncIntegrationsOnRemote`
- Added check to hide sync integrations feature in serverless
- Moved creating the follower index from Fleet setup to output
create/update API and async task (create if does not exist)
- Follower index is not hidden for now, because if hidden, it's not
showing up on the CCR UI

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Julia Bardi 2025-06-04 12:18:21 +02:00 committed by GitHub
parent 013384e06d
commit d65f9c5d25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1276 additions and 147 deletions

View file

@ -45110,6 +45110,339 @@
] ]
} }
}, },
"/api/fleet/remote_synced_integrations/status": {
"get": {
"description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.",
"operationId": "get-fleet-remote-synced-integrations-status",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"custom_assets": {
"additionalProperties": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"is_deleted": {
"type": "boolean"
},
"name": {
"type": "string"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"type",
"name",
"package_name",
"package_version",
"sync_status"
],
"type": "object"
},
"type": "object"
},
"error": {
"type": "string"
},
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"id": {
"type": "string"
},
"install_status": {
"additionalProperties": false,
"properties": {
"main": {
"type": "string"
},
"remote": {
"type": "string"
}
},
"required": [
"main"
],
"type": "object"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"updated_at": {
"type": "string"
},
"warning": {
"type": "string"
}
},
"required": [
"sync_status",
"install_status"
],
"type": "object"
},
"type": "array"
},
"warning": {
"type": "string"
}
},
"required": [
"integrations"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"attributes": {},
"error": {
"type": "string"
},
"errorType": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message",
"attributes"
],
"type": "object"
}
}
}
}
},
"summary": "Get CCR Remote synced integrations status",
"tags": [
"CCR Remote synced integrations"
]
}
},
"/api/fleet/remote_synced_integrations/{outputId}/remote_status": {
"get": {
"description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.",
"operationId": "get-fleet-remote-synced-integrations-outputid-remote-status",
"parameters": [
{
"in": "path",
"name": "outputId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"custom_assets": {
"additionalProperties": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"is_deleted": {
"type": "boolean"
},
"name": {
"type": "string"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"type",
"name",
"package_name",
"package_version",
"sync_status"
],
"type": "object"
},
"type": "object"
},
"error": {
"type": "string"
},
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"id": {
"type": "string"
},
"install_status": {
"additionalProperties": false,
"properties": {
"main": {
"type": "string"
},
"remote": {
"type": "string"
}
},
"required": [
"main"
],
"type": "object"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"updated_at": {
"type": "string"
},
"warning": {
"type": "string"
}
},
"required": [
"sync_status",
"install_status"
],
"type": "object"
},
"type": "array"
},
"warning": {
"type": "string"
}
},
"required": [
"integrations"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"attributes": {},
"error": {
"type": "string"
},
"errorType": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message",
"attributes"
],
"type": "object"
}
}
}
}
},
"summary": "Get CCR Remote synced integrations status by outputId",
"tags": [
"CCR Remote synced integrations"
]
}
},
"/api/fleet/service_tokens": { "/api/fleet/service_tokens": {
"post": { "post": {
"description": "[Required authorization] Route required privileges: fleet-agents-all.", "description": "[Required authorization] Route required privileges: fleet-agents-all.",
@ -60516,6 +60849,9 @@
{ {
"name": "alerting" "name": "alerting"
}, },
{
"name": "CCR Remote synced integrations"
},
{ {
"name": "connectors" "name": "connectors"
}, },

View file

@ -45110,6 +45110,339 @@
] ]
} }
}, },
"/api/fleet/remote_synced_integrations/status": {
"get": {
"description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.",
"operationId": "get-fleet-remote-synced-integrations-status",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"custom_assets": {
"additionalProperties": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"is_deleted": {
"type": "boolean"
},
"name": {
"type": "string"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"type",
"name",
"package_name",
"package_version",
"sync_status"
],
"type": "object"
},
"type": "object"
},
"error": {
"type": "string"
},
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"id": {
"type": "string"
},
"install_status": {
"additionalProperties": false,
"properties": {
"main": {
"type": "string"
},
"remote": {
"type": "string"
}
},
"required": [
"main"
],
"type": "object"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"updated_at": {
"type": "string"
},
"warning": {
"type": "string"
}
},
"required": [
"sync_status",
"install_status"
],
"type": "object"
},
"type": "array"
},
"warning": {
"type": "string"
}
},
"required": [
"integrations"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"attributes": {},
"error": {
"type": "string"
},
"errorType": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message",
"attributes"
],
"type": "object"
}
}
}
}
},
"summary": "Get CCR Remote synced integrations status",
"tags": [
"CCR Remote synced integrations"
]
}
},
"/api/fleet/remote_synced_integrations/{outputId}/remote_status": {
"get": {
"description": "[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.",
"operationId": "get-fleet-remote-synced-integrations-outputid-remote-status",
"parameters": [
{
"in": "path",
"name": "outputId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"custom_assets": {
"additionalProperties": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"is_deleted": {
"type": "boolean"
},
"name": {
"type": "string"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"type": {
"type": "string"
}
},
"required": [
"type",
"name",
"package_name",
"package_version",
"sync_status"
],
"type": "object"
},
"type": "object"
},
"error": {
"type": "string"
},
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"error": {
"type": "string"
},
"id": {
"type": "string"
},
"install_status": {
"additionalProperties": false,
"properties": {
"main": {
"type": "string"
},
"remote": {
"type": "string"
}
},
"required": [
"main"
],
"type": "object"
},
"package_name": {
"type": "string"
},
"package_version": {
"type": "string"
},
"sync_status": {
"enum": [
"completed",
"synchronizing",
"failed",
"warning"
],
"type": "string"
},
"updated_at": {
"type": "string"
},
"warning": {
"type": "string"
}
},
"required": [
"sync_status",
"install_status"
],
"type": "object"
},
"type": "array"
},
"warning": {
"type": "string"
}
},
"required": [
"integrations"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"attributes": {},
"error": {
"type": "string"
},
"errorType": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message",
"attributes"
],
"type": "object"
}
}
}
}
},
"summary": "Get CCR Remote synced integrations status by outputId",
"tags": [
"CCR Remote synced integrations"
]
}
},
"/api/fleet/service_tokens": { "/api/fleet/service_tokens": {
"post": { "post": {
"description": "[Required authorization] Route required privileges: fleet-agents-all.", "description": "[Required authorization] Route required privileges: fleet-agents-all.",
@ -60107,6 +60440,9 @@
{ {
"name": "alerting" "name": "alerting"
}, },
{
"name": "CCR Remote synced integrations"
},
{ {
"name": "connectors" "name": "connectors"
}, },

View file

@ -66,6 +66,7 @@ tags:
Configure APM source maps. A source map allows minified files to be mapped back to original source code--allowing you to maintain the speed advantage of minified code, without losing the ability to quickly and easily debug your application. Configure APM source maps. A source map allows minified files to be mapped back to original source code--allowing you to maintain the speed advantage of minified code, without losing the ability to quickly and easily debug your application.
For best results, uploading source maps should become a part of your deployment procedure, and not something you only do when you see unhelpful errors. That's because uploading source maps after errors happen won't make old errors magically readable--errors must occur again for source mapping to occur. For best results, uploading source maps should become a part of your deployment procedure, and not something you only do when you see unhelpful errors. That's because uploading source maps after errors happen won't make old errors magically readable--errors must occur again for source mapping to occur.
name: APM sourcemaps name: APM sourcemaps
- name: CCR Remote synced integrations
- name: connectors - name: connectors
description: | description: |
Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met. Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met.
@ -39651,6 +39652,233 @@ paths:
summary: Update a proxy summary: Update a proxy
tags: tags:
- Fleet proxies - Fleet proxies
/api/fleet/remote_synced_integrations/{outputId}/remote_status:
get:
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
operationId: get-fleet-remote-synced-integrations-outputid-remote-status
parameters:
- in: path
name: outputId
required: true
schema:
type: string
responses:
'200':
content:
application/json:
schema:
additionalProperties: false
type: object
properties:
custom_assets:
additionalProperties:
additionalProperties: false
type: object
properties:
error:
type: string
is_deleted:
type: boolean
name:
type: string
package_name:
type: string
package_version:
type: string
sync_status:
enum:
- completed
- synchronizing
- failed
- warning
type: string
type:
type: string
required:
- type
- name
- package_name
- package_version
- sync_status
type: object
error:
type: string
integrations:
items:
additionalProperties: false
type: object
properties:
error:
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:
type: string
sync_status:
enum:
- 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':
content:
application/json:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
attributes: {}
error:
type: string
errorType:
type: string
message:
type: string
statusCode:
type: number
required:
- message
- attributes
summary: Get CCR Remote synced integrations status by outputId
tags:
- CCR Remote synced integrations
/api/fleet/remote_synced_integrations/status:
get:
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
operationId: get-fleet-remote-synced-integrations-status
parameters: []
responses:
'200':
content:
application/json:
schema:
additionalProperties: false
type: object
properties:
custom_assets:
additionalProperties:
additionalProperties: false
type: object
properties:
error:
type: string
is_deleted:
type: boolean
name:
type: string
package_name:
type: string
package_version:
type: string
sync_status:
enum:
- completed
- synchronizing
- failed
- warning
type: string
type:
type: string
required:
- type
- name
- package_name
- package_version
- sync_status
type: object
error:
type: string
integrations:
items:
additionalProperties: false
type: object
properties:
error:
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:
type: string
sync_status:
enum:
- 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':
content:
application/json:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
attributes: {}
error:
type: string
errorType:
type: string
message:
type: string
statusCode:
type: number
required:
- message
- attributes
summary: Get CCR Remote synced integrations status
tags:
- CCR Remote synced integrations
/api/fleet/service_tokens: /api/fleet/service_tokens:
post: post:
description: '[Required authorization] Route required privileges: fleet-agents-all.' description: '[Required authorization] Route required privileges: fleet-agents-all.'

View file

@ -80,6 +80,7 @@ tags:
description: Cases documentation description: Cases documentation
url: https://www.elastic.co/docs/explore-analyze/alerts-cases/cases url: https://www.elastic.co/docs/explore-analyze/alerts-cases/cases
x-displayName: Cases x-displayName: Cases
- name: CCR Remote synced integrations
- name: connectors - name: connectors
description: | description: |
Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met. Connectors provide a central place to store connection information for services and integrations with Elastic or third party systems. Alerting rules can use connectors to run actions when rule conditions are met.
@ -41893,6 +41894,233 @@ paths:
summary: Update a proxy summary: Update a proxy
tags: tags:
- Fleet proxies - Fleet proxies
/api/fleet/remote_synced_integrations/{outputId}/remote_status:
get:
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
operationId: get-fleet-remote-synced-integrations-outputid-remote-status
parameters:
- in: path
name: outputId
required: true
schema:
type: string
responses:
'200':
content:
application/json:
schema:
additionalProperties: false
type: object
properties:
custom_assets:
additionalProperties:
additionalProperties: false
type: object
properties:
error:
type: string
is_deleted:
type: boolean
name:
type: string
package_name:
type: string
package_version:
type: string
sync_status:
enum:
- completed
- synchronizing
- failed
- warning
type: string
type:
type: string
required:
- type
- name
- package_name
- package_version
- sync_status
type: object
error:
type: string
integrations:
items:
additionalProperties: false
type: object
properties:
error:
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:
type: string
sync_status:
enum:
- 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':
content:
application/json:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
attributes: {}
error:
type: string
errorType:
type: string
message:
type: string
statusCode:
type: number
required:
- message
- attributes
summary: Get CCR Remote synced integrations status by outputId
tags:
- CCR Remote synced integrations
/api/fleet/remote_synced_integrations/status:
get:
description: '[Required authorization] Route required privileges: fleet-settings-read AND integrations-read.'
operationId: get-fleet-remote-synced-integrations-status
parameters: []
responses:
'200':
content:
application/json:
schema:
additionalProperties: false
type: object
properties:
custom_assets:
additionalProperties:
additionalProperties: false
type: object
properties:
error:
type: string
is_deleted:
type: boolean
name:
type: string
package_name:
type: string
package_version:
type: string
sync_status:
enum:
- completed
- synchronizing
- failed
- warning
type: string
type:
type: string
required:
- type
- name
- package_name
- package_version
- sync_status
type: object
error:
type: string
integrations:
items:
additionalProperties: false
type: object
properties:
error:
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:
type: string
sync_status:
enum:
- 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':
content:
application/json:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
attributes: {}
error:
type: string
errorType:
type: string
message:
type: string
statusCode:
type: number
required:
- message
- attributes
summary: Get CCR Remote synced integrations status
tags:
- CCR Remote synced integrations
/api/fleet/service_tokens: /api/fleet/service_tokens:
post: post:
description: '[Required authorization] Route required privileges: fleet-agents-all.' description: '[Required authorization] Route required privileges: fleet-agents-all.'

View file

@ -11,7 +11,7 @@ const _allowedExperimentalValues = {
showExperimentalShipperOptions: false, showExperimentalShipperOptions: false,
useSpaceAwareness: false, useSpaceAwareness: false,
enableAutomaticAgentUpgrades: true, enableAutomaticAgentUpgrades: true,
enableSyncIntegrationsOnRemote: false, enableSyncIntegrationsOnRemote: true,
enableSSLSecrets: false, enableSSLSecrets: false,
installedIntegrationsTabularUI: false, installedIntegrationsTabularUI: false,
enabledUpgradeAgentlessDeploymentsTask: false, enabledUpgradeAgentlessDeploymentsTask: false,

View file

@ -106,6 +106,24 @@ Save the responses as they will be required in Cluster A (see next section).
- Choose a name, put `localhost:9300` for "Seed nodes", and save (check "Yes, I have setup trust") - Choose a name, put `localhost:9300` for "Seed nodes", and save (check "Yes, I have setup trust")
- Make sure the connection status is "Connected" - Make sure the connection status is "Connected"
- Equivalent Dev Tools API request
```
PUT /_cluster/settings
{
"persistent" : {
"cluster" : {
"remote" : {
"local" : {
"seeds" : [
"localhost:9300"
]
}
}
}
}
}
```
### Set up CCR ### Set up CCR
Please note that [CCR](https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-getting-started-tutorial.html) requires both clusters to have the same license. At the time of writing an `enterprise` license is needed. Please note that [CCR](https://www.elastic.co/guide/en/elasticsearch/reference/current/ccr-getting-started-tutorial.html) requires both clusters to have the same license. At the time of writing an `enterprise` license is needed.
@ -114,6 +132,16 @@ On cluster 1, navigate to *Stack Management > Cross-Cluster Replication* and c
- Leader index `fleet-synced-integrations` - Leader index `fleet-synced-integrations`
- Follower index `fleet-synced-integrations-ccr-remote1` - Follower index `fleet-synced-integrations-ccr-remote1`
- Equivalent Dev Tools API request
```
PUT /fleet-synced-integrations-ccr-local/_ccr/follow
{
"remote_cluster" : "local",
"leader_index" : "fleet-synced-integrations"
}
```
### Set up local ES output ### Set up local ES output
This configuration is required to kick off the integration sync. The local host needs to match the remote ES output configured on A (see next section). Note that `kibana.dev.yml` is read by both kibana instances so it's better to add it in the UI to avoid conflicts. This configuration is required to kick off the integration sync. The local host needs to match the remote ES output configured on A (see next section). Note that `kibana.dev.yml` is read by both kibana instances so it's better to add it in the UI to avoid conflicts.

View file

@ -46,7 +46,7 @@ export interface IsConvertedToSecret {
} }
export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props) => { export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props) => {
const { docLinks } = useStartServices(); const { docLinks, cloud } = useStartServices();
const { inputs, useSecretsStorage, onToggleSecretStorage } = props; const { inputs, useSecretsStorage, onToggleSecretStorage } = props;
const [isConvertedToSecret, setIsConvertedToSecret] = React.useState<IsConvertedToSecret>({ const [isConvertedToSecret, setIsConvertedToSecret] = React.useState<IsConvertedToSecret>({
serviceToken: false, serviceToken: false,
@ -54,7 +54,8 @@ export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props)
sslKey: false, sslKey: false,
}); });
const { enableSyncIntegrationsOnRemote, enableSSLSecrets } = ExperimentalFeaturesService.get(); const { enableSyncIntegrationsOnRemote, enableSSLSecrets } = ExperimentalFeaturesService.get();
const enableSyncIntegrations = enableSyncIntegrationsOnRemote && licenseService.isEnterprise(); const enableSyncIntegrations =
enableSyncIntegrationsOnRemote && licenseService.isEnterprise() && !cloud?.isServerlessEnabled;
const [isRemoteClusterInstructionsOpen, setIsRemoteClusterInstructionsOpen] = const [isRemoteClusterInstructionsOpen, setIsRemoteClusterInstructionsOpen] =
React.useState(false); React.useState(false);

View file

@ -11,7 +11,7 @@ import { EuiBasicTable, EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiIconTip } f
import type { EuiBasicTableColumn } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui';
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { licenseService, useAuthz, useLink } from '../../../../hooks'; import { licenseService, useAuthz, useLink, useStartServices } from '../../../../hooks';
import type { Output } from '../../../../types'; import type { Output } from '../../../../types';
import { OutputHealth } from '../edit_output_flyout/output_health'; import { OutputHealth } from '../edit_output_flyout/output_health';
@ -57,7 +57,9 @@ export const OutputsTable: React.FunctionComponent<OutputsTableProps> = ({
const authz = useAuthz(); const authz = useAuthz();
const { getHref } = useLink(); const { getHref } = useLink();
const { enableSyncIntegrationsOnRemote } = ExperimentalFeaturesService.get(); const { enableSyncIntegrationsOnRemote } = ExperimentalFeaturesService.get();
const enableSyncIntegrations = enableSyncIntegrationsOnRemote && licenseService.isEnterprise(); const { cloud } = useStartServices();
const enableSyncIntegrations =
enableSyncIntegrationsOnRemote && licenseService.isEnterprise() && !cloud?.isServerlessEnabled;
const columns = useMemo((): Array<EuiBasicTableColumn<Output>> => { const columns = useMemo((): Array<EuiBasicTableColumn<Output>> => {
return [ return [

View file

@ -15,81 +15,77 @@ import { GetRemoteSyncedIntegrationsStatusResponseSchema } from '../../types/mod
import { GetRemoteSyncedIntegrationsInfoRequestSchema } from '../../types'; import { GetRemoteSyncedIntegrationsInfoRequestSchema } from '../../types';
import { canEnableSyncIntegrations } from '../../services/setup/fleet_synced_integrations';
import { import {
getRemoteSyncedIntegrationsStatusHandler, getRemoteSyncedIntegrationsStatusHandler,
getRemoteSyncedIntegrationsInfoHandler, getRemoteSyncedIntegrationsInfoHandler,
} from './handler'; } from './handler';
export const registerRoutes = (router: FleetAuthzRouter) => { export const registerRoutes = (router: FleetAuthzRouter) => {
if (canEnableSyncIntegrations()) { router.versioned
router.versioned .get({
.get({ path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.STATUS_PATTERN,
path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.STATUS_PATTERN, security: {
security: { authz: {
authz: { requiredPrivileges: [
requiredPrivileges: [ FLEET_API_PRIVILEGES.SETTINGS.READ,
FLEET_API_PRIVILEGES.SETTINGS.READ, FLEET_API_PRIVILEGES.INTEGRATIONS.READ,
FLEET_API_PRIVILEGES.INTEGRATIONS.READ, ],
],
},
}, },
summary: `Get CCR Remote synced integrations status`, },
options: { summary: `Get CCR Remote synced integrations status`,
tags: ['oas-tag:CCR Remote synced integrations'], options: {
}, tags: ['oas-tag:CCR Remote synced integrations'],
}) },
.addVersion( })
{ .addVersion(
version: API_VERSIONS.public.v1, {
validate: { version: API_VERSIONS.public.v1,
request: {}, validate: {
response: { request: {},
200: { response: {
body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, 200: {
}, body: () => GetRemoteSyncedIntegrationsStatusResponseSchema,
400: { },
body: genericErrorResponse, 400: {
}, body: genericErrorResponse,
}, },
}, },
}, },
getRemoteSyncedIntegrationsStatusHandler },
); getRemoteSyncedIntegrationsStatusHandler
);
router.versioned router.versioned
.get({ .get({
path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.INFO_PATTERN, path: REMOTE_SYNCED_INTEGRATIONS_API_ROUTES.INFO_PATTERN,
security: { security: {
authz: { authz: {
requiredPrivileges: [ requiredPrivileges: [
FLEET_API_PRIVILEGES.SETTINGS.READ, FLEET_API_PRIVILEGES.SETTINGS.READ,
FLEET_API_PRIVILEGES.INTEGRATIONS.READ, FLEET_API_PRIVILEGES.INTEGRATIONS.READ,
], ],
},
}, },
summary: `Get CCR Remote synced integrations status by outputId`, },
options: { summary: `Get CCR Remote synced integrations status by outputId`,
tags: ['oas-tag:CCR Remote synced integrations'], options: {
}, tags: ['oas-tag:CCR Remote synced integrations'],
}) },
.addVersion( })
{ .addVersion(
version: API_VERSIONS.public.v1, {
validate: { version: API_VERSIONS.public.v1,
request: GetRemoteSyncedIntegrationsInfoRequestSchema, validate: {
response: { request: GetRemoteSyncedIntegrationsInfoRequestSchema,
200: { response: {
body: () => GetRemoteSyncedIntegrationsStatusResponseSchema, 200: {
}, body: () => GetRemoteSyncedIntegrationsStatusResponseSchema,
400: { },
body: genericErrorResponse, 400: {
}, body: genericErrorResponse,
}, },
}, },
}, },
getRemoteSyncedIntegrationsInfoHandler },
); getRemoteSyncedIntegrationsInfoHandler
} );
}; };

View file

@ -83,7 +83,10 @@ import {
} from './secrets'; } from './secrets';
import { findAgentlessPolicies } from './outputs/helpers'; import { findAgentlessPolicies } from './outputs/helpers';
import { patchUpdateDataWithRequireEncryptedAADFields } from './outputs/so_helpers'; import { patchUpdateDataWithRequireEncryptedAADFields } from './outputs/so_helpers';
import { canEnableSyncIntegrations } from './setup/fleet_synced_integrations'; import {
canEnableSyncIntegrations,
createOrUpdateFleetSyncedIntegrationsIndex,
} from './setup/fleet_synced_integrations';
type Nullable<T> = { [P in keyof T]: T[P] | null }; type Nullable<T> = { [P in keyof T]: T[P] | null };
@ -368,15 +371,18 @@ async function updateAgentPoliciesDataOutputId(
} }
} }
function validateRemoteSyncIntegrationsCanBeEnabled(output: Partial<NewOutput>) { async function remoteSyncIntegrationsCheck(
if ( esClient: ElasticsearchClient,
output.type === outputType.RemoteElasticsearch && output: Partial<NewOutput>
(output.sync_integrations === true || output.sync_uninstalled_integrations === true) && ) {
!canEnableSyncIntegrations() const syncIntegrationsEnabled =
) { output.type === outputType.RemoteElasticsearch && output.sync_integrations === true;
if (syncIntegrationsEnabled && !canEnableSyncIntegrations()) {
throw new OutputUnauthorizedError( throw new OutputUnauthorizedError(
'Remote sync integrations require at least an Enterprise license.' 'Remote sync integrations require at least an Enterprise license.'
); );
} else if (syncIntegrationsEnabled) {
await createOrUpdateFleetSyncedIntegrationsIndex(esClient);
} }
} }
@ -692,7 +698,7 @@ class OutputService {
} }
} }
validateRemoteSyncIntegrationsCanBeEnabled(output); await remoteSyncIntegrationsCheck(esClient, output);
const id = options?.id ? outputIdToUuid(options.id) : SavedObjectsUtils.generateId(); const id = options?.id ? outputIdToUuid(options.id) : SavedObjectsUtils.generateId();
@ -1118,7 +1124,7 @@ class OutputService {
updateData.shipper = null; updateData.shipper = null;
} }
} }
validateRemoteSyncIntegrationsCanBeEnabled(data); await remoteSyncIntegrationsCheck(esClient, data);
// Store secret values if enabled; if not, store plain text values // Store secret values if enabled; if not, store plain text values
if (await isOutputSecretStorageEnabled(esClient, soClient)) { if (await isOutputSecretStorageEnabled(esClient, soClient)) {

View file

@ -24,10 +24,7 @@ import { getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig } from './preco
import { _runSetupWithLock, setupFleet } from './setup'; import { _runSetupWithLock, setupFleet } from './setup';
import { isPackageInstalled } from './epm/packages/install'; import { isPackageInstalled } from './epm/packages/install';
import { upgradeAgentPolicySchemaVersion } from './setup/upgrade_agent_policy_schema_version'; import { upgradeAgentPolicySchemaVersion } from './setup/upgrade_agent_policy_schema_version';
import { import { createCCSIndexPatterns } from './setup/fleet_synced_integrations';
createCCSIndexPatterns,
createOrUpdateFleetSyncedIntegrationsIndex,
} from './setup/fleet_synced_integrations';
import { getSpaceAwareSaveobjectsClients } from './epm/kibana/assets/saved_objects'; import { getSpaceAwareSaveobjectsClients } from './epm/kibana/assets/saved_objects';
jest.mock('./app_context'); jest.mock('./app_context');
@ -107,7 +104,6 @@ describe('setupFleet', () => {
(getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig as jest.Mock).mockResolvedValue([]); (getPreconfiguredDeleteUnenrolledAgentsSettingFromConfig as jest.Mock).mockResolvedValue([]);
(isPackageInstalled as jest.Mock).mockResolvedValue(true); (isPackageInstalled as jest.Mock).mockResolvedValue(true);
(upgradeAgentPolicySchemaVersion as jest.Mock).mockResolvedValue(undefined); (upgradeAgentPolicySchemaVersion as jest.Mock).mockResolvedValue(undefined);
(createOrUpdateFleetSyncedIntegrationsIndex as jest.Mock).mockResolvedValue(undefined);
(createCCSIndexPatterns as jest.Mock).mockResolvedValue(undefined); (createCCSIndexPatterns as jest.Mock).mockResolvedValue(undefined);
(getSpaceAwareSaveobjectsClients as jest.Mock).mockReturnValue({}); (getSpaceAwareSaveobjectsClients as jest.Mock).mockReturnValue({});
}); });

View file

@ -66,10 +66,7 @@ import {
} from './preconfiguration/delete_unenrolled_agent_setting'; } from './preconfiguration/delete_unenrolled_agent_setting';
import { backfillPackagePolicySupportsAgentless } from './backfill_agentless'; import { backfillPackagePolicySupportsAgentless } from './backfill_agentless';
import { updateDeprecatedComponentTemplates } from './setup/update_deprecated_component_templates'; import { updateDeprecatedComponentTemplates } from './setup/update_deprecated_component_templates';
import { import { createCCSIndexPatterns } from './setup/fleet_synced_integrations';
createCCSIndexPatterns,
createOrUpdateFleetSyncedIntegrationsIndex,
} from './setup/fleet_synced_integrations';
import { ensureCorrectAgentlessSettingsIds } from './agentless_settings_ids'; import { ensureCorrectAgentlessSettingsIds } from './agentless_settings_ids';
import { getSpaceAwareSaveobjectsClients } from './epm/kibana/assets/saved_objects'; import { getSpaceAwareSaveobjectsClients } from './epm/kibana/assets/saved_objects';
@ -281,9 +278,6 @@ async function createSetupSideEffects(
logger.debug('Update deprecated _source.mode in component templates'); logger.debug('Update deprecated _source.mode in component templates');
await updateDeprecatedComponentTemplates(esClient); await updateDeprecatedComponentTemplates(esClient);
logger.debug('Create or update fleet-synced-integrations index');
await createOrUpdateFleetSyncedIntegrationsIndex(esClient);
logger.debug('Create CCS index patterns for remote clusters'); logger.debug('Create CCS index patterns for remote clusters');
const { savedObjectsImporter } = getSpaceAwareSaveobjectsClients(); const { savedObjectsImporter } = getSpaceAwareSaveobjectsClients();
await createCCSIndexPatterns(esClient, soClient, savedObjectsImporter); await createCCSIndexPatterns(esClient, soClient, savedObjectsImporter);

View file

@ -17,10 +17,14 @@ jest.mock('../app_context', () => ({
getExperimentalFeatures: jest.fn().mockReturnValue({ enableSyncIntegrationsOnRemote: true }), getExperimentalFeatures: jest.fn().mockReturnValue({ enableSyncIntegrationsOnRemote: true }),
getLogger: jest.fn().mockReturnValue({ getLogger: jest.fn().mockReturnValue({
error: jest.fn(), error: jest.fn(),
debug: jest.fn(),
}), }),
getConfig: jest.fn().mockReturnValue({ getConfig: jest.fn().mockReturnValue({
enableManagedLogsAndMetricsDataviews: true, enableManagedLogsAndMetricsDataviews: true,
}), }),
getCloud: jest.fn().mockReturnValue({
isServerlessEnabled: false,
}),
}, },
})); }));
@ -147,32 +151,6 @@ describe('fleet_synced_integrations', () => {
jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false); jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false);
}); });
it('should not create index', async () => {
mockExists.mockResolvedValue(false);
jest.spyOn(licenseService, 'isEnterprise').mockReturnValue(false);
await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock);
expect(esClientMock.indices.create).not.toHaveBeenCalled();
});
it('should not update index', async () => {
mockExists.mockResolvedValue(true);
mockGetMapping.mockResolvedValue({
'fleet-synced-integrations': {
mappings: {
_meta: {
version: '0.0',
},
},
},
});
await createOrUpdateFleetSyncedIntegrationsIndex(esClientMock);
expect(esClientMock.indices.putMapping).not.toHaveBeenCalled();
});
it('should not create index patterns for remote clusters', async () => { it('should not create index patterns for remote clusters', async () => {
await createCCSIndexPatterns(esClientMock, soClientMock, soImporterMock); await createCCSIndexPatterns(esClientMock, soClientMock, soImporterMock);

View file

@ -67,14 +67,12 @@ export const FLEET_SYNCED_INTEGRATIONS_INDEX_CONFIG = {
export const canEnableSyncIntegrations = () => { export const canEnableSyncIntegrations = () => {
const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures(); const { enableSyncIntegrationsOnRemote } = appContextService.getExperimentalFeatures();
return enableSyncIntegrationsOnRemote && licenseService.isEnterprise(); const isServerless = appContextService.getCloud()?.isServerlessEnabled;
return enableSyncIntegrationsOnRemote && licenseService.isEnterprise() && !isServerless;
}; };
export async function createOrUpdateFleetSyncedIntegrationsIndex(esClient: ElasticsearchClient) { export async function createOrUpdateFleetSyncedIntegrationsIndex(esClient: ElasticsearchClient) {
if (!canEnableSyncIntegrations()) { appContextService.getLogger().debug('Create or update fleet-synced-integrations index');
return;
}
await createOrUpdateIndex( await createOrUpdateIndex(
esClient, esClient,
FLEET_SYNCED_INTEGRATIONS_INDEX_NAME, FLEET_SYNCED_INTEGRATIONS_INDEX_NAME,

View file

@ -39,6 +39,7 @@ export const getFollowerIndex = async (
const indices = await esClient.indices.get( const indices = await esClient.indices.get(
{ {
index: FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX, index: FLEET_SYNCED_INTEGRATIONS_CCR_INDEX_PREFIX,
expand_wildcards: 'all',
}, },
{ signal: abortController.signal } { signal: abortController.signal }
); );

View file

@ -40,6 +40,10 @@ jest.mock('../../services/app_context', () => ({
appContextService: { appContextService: {
getExperimentalFeatures: jest.fn().mockReturnValue({ enableSyncIntegrationsOnRemote: true }), getExperimentalFeatures: jest.fn().mockReturnValue({ enableSyncIntegrationsOnRemote: true }),
start: jest.fn(), start: jest.fn(),
getCloud: jest.fn().mockReturnValue({ isServerlessEnabled: false }),
getLogger: jest.fn().mockReturnValue({
debug: jest.fn(),
}),
}, },
})); }));
@ -145,6 +149,15 @@ describe('SyncIntegrationsTask', () => {
const [{ elasticsearch }] = await mockCore.getStartServices(); const [{ elasticsearch }] = await mockCore.getStartServices();
esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock; esClient = elasticsearch.client.asInternalUser as ElasticsearchClientMock;
esClient.indices.exists.mockResolvedValue(true); esClient.indices.exists.mockResolvedValue(true);
esClient.indices.getMapping.mockResolvedValue({
'fleet-synced-integrations': {
mappings: {
_meta: {
version: '1.0',
},
},
},
});
esClient.cluster.getComponentTemplate.mockResolvedValue({ esClient.cluster.getComponentTemplate.mockResolvedValue({
component_templates: [ component_templates: [
{ {

View file

@ -24,6 +24,7 @@ import { getInstalledPackageSavedObjects } from '../../services/epm/packages/get
import { import {
FLEET_SYNCED_INTEGRATIONS_INDEX_NAME, FLEET_SYNCED_INTEGRATIONS_INDEX_NAME,
canEnableSyncIntegrations, canEnableSyncIntegrations,
createOrUpdateFleetSyncedIntegrationsIndex,
} from '../../services/setup/fleet_synced_integrations'; } from '../../services/setup/fleet_synced_integrations';
import { syncIntegrationsOnRemote } from './sync_integrations_on_remote'; import { syncIntegrationsOnRemote } from './sync_integrations_on_remote';
@ -31,7 +32,7 @@ import { getCustomAssets } from './custom_assets';
import type { SyncIntegrationsData } from './model'; import type { SyncIntegrationsData } from './model';
export const TYPE = 'fleet:sync-integrations-task'; export const TYPE = 'fleet:sync-integrations-task';
export const VERSION = '1.0.4'; export const VERSION = '1.0.5';
const TITLE = 'Fleet Sync Integrations Task'; const TITLE = 'Fleet Sync Integrations Task';
const SCOPE = ['fleet']; const SCOPE = ['fleet'];
const INTERVAL = '5m'; const INTERVAL = '5m';
@ -122,14 +123,14 @@ export class SyncIntegrationsTask {
this.logger.info(`[runTask()] started`); this.logger.info(`[runTask()] started`);
const [coreStart, _startDeps, { packageService }] = (await core.getStartServices()) as any;
const esClient = coreStart.elasticsearch.client.asInternalUser;
const soClient = new SavedObjectsClient(coreStart.savedObjects.createInternalRepository());
if (!canEnableSyncIntegrations()) { if (!canEnableSyncIntegrations()) {
return; return;
} }
const [coreStart, _startDeps, { packageService }] = (await core.getStartServices()) as any;
const esClient = coreStart.elasticsearch.client.asInternalUser;
const soClient = new SavedObjectsClient(coreStart.savedObjects.createInternalRepository());
try { try {
// write integrations on main cluster // write integrations on main cluster
await this.updateSyncedIntegrationsData(esClient, soClient); await this.updateSyncedIntegrationsData(esClient, soClient);
@ -155,15 +156,6 @@ export class SyncIntegrationsTask {
} }
}; };
private syncedIntegrationsIndexExists = async (esClient: ElasticsearchClient) => {
return await esClient.indices.exists(
{
index: FLEET_SYNCED_INTEGRATIONS_INDEX_NAME,
},
{ signal: this.abortController.signal }
);
};
private getSyncedIntegrationDoc = async ( private getSyncedIntegrationDoc = async (
esClient: ElasticsearchClient esClient: ElasticsearchClient
): Promise<SyncIntegrationsData | undefined> => { ): Promise<SyncIntegrationsData | undefined> => {
@ -191,15 +183,6 @@ export class SyncIntegrationsTask {
esClient: ElasticsearchClient, esClient: ElasticsearchClient,
soClient: SavedObjectsClient soClient: SavedObjectsClient
) => { ) => {
const indexExists = await this.syncedIntegrationsIndexExists(esClient);
if (!indexExists) {
this.logger.info(
`[SyncIntegrationsTask] index ${FLEET_SYNCED_INTEGRATIONS_INDEX_NAME} does not exist`
);
return;
}
const outputs = await outputService.list(soClient); const outputs = await outputService.list(soClient);
const remoteESOutputs = outputs.items.filter( const remoteESOutputs = outputs.items.filter(
(output) => output.type === outputType.RemoteElasticsearch (output) => output.type === outputType.RemoteElasticsearch
@ -208,6 +191,10 @@ export class SyncIntegrationsTask {
(output) => (output as NewRemoteElasticsearchOutput).sync_integrations (output) => (output as NewRemoteElasticsearchOutput).sync_integrations
); );
if (isSyncEnabled) {
await createOrUpdateFleetSyncedIntegrationsIndex(esClient);
}
const previousSyncIntegrationsData = await this.getSyncedIntegrationDoc(esClient); const previousSyncIntegrationsData = await this.getSyncedIntegrationDoc(esClient);
if (!isSyncEnabled) { if (!isSyncEnabled) {

View file

@ -792,7 +792,7 @@ export default function (providerContext: FtrProviderContext) {
} }
}); });
it('should not allow to update kibana_api_key on an existing remote_elasticsearch output if the license is not at least enterprise', async function () { it('should allow to update kibana_api_key on an existing remote_elasticsearch output', async function () {
const res = await supertest const res = await supertest
.post(`/api/fleet/outputs`) .post(`/api/fleet/outputs`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
@ -804,7 +804,7 @@ export default function (providerContext: FtrProviderContext) {
}) })
.expect(200); .expect(200);
const outputId = res.body.item.id; const outputId = res.body.item.id;
await supertest const updatedRes = await supertest
.put(`/api/fleet/outputs/${outputId}`) .put(`/api/fleet/outputs/${outputId}`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
.send({ .send({
@ -815,7 +815,8 @@ export default function (providerContext: FtrProviderContext) {
kibana_url: 'https://testhost', kibana_url: 'https://testhost',
kibana_api_key: 'bbbb', kibana_api_key: 'bbbb',
}) })
.expect(400); .expect(200);
expect(updatedRes.body.item.kibana_api_key).to.equal('bbbb');
}); });
it('should bump all policies in all spaces if updating the default output', async () => { it('should bump all policies in all spaces if updating the default output', async () => {
@ -1821,7 +1822,7 @@ export default function (providerContext: FtrProviderContext) {
.expect(200); .expect(200);
}); });
it('should not allow to create a new remote_elasticsearch output with kibana_api_key if the license is not at least enterprise', async function () { it('should allow to create a new remote_elasticsearch output with kibana_api_key field', async function () {
await supertest await supertest
.post(`/api/fleet/outputs`) .post(`/api/fleet/outputs`)
.set('kbn-xsrf', 'xxxx') .set('kbn-xsrf', 'xxxx')
@ -1833,7 +1834,7 @@ export default function (providerContext: FtrProviderContext) {
kibana_url: 'https://testhost', kibana_url: 'https://testhost',
kibana_api_key: 'aaaa', kibana_api_key: 'aaaa',
}) })
.expect(400); .expect(200);
}); });
}); });