Skip to content

Commit

Permalink
[UII] Add remote cluster instructions for syncing integrations (#211997)
Browse files Browse the repository at this point in the history
## Summary

Resolves #206239. This PR adds
instructions for configuring the remote cluster to enable syncing
integrations (for a Remote Elasticsearch output).

It also hides extra fields when `Sync integrations` is not enabled:


![image](https://github.com/user-attachments/assets/fc76050c-fdc7-4d5f-b169-8fcb97c65b4e)

When enabled: 


![image](https://github.com/user-attachments/assets/23a28086-598c-478b-a898-ad0214c45b37)

### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md)
  • Loading branch information
jen-huang authored Feb 21, 2025
1 parent c0da61a commit f0797db
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
installAndUninstallIntegrationAssets: `${FLEET_DOCS}install-uninstall-integration-assets.html`,
elasticAgentInputConfiguration: `${FLEET_DOCS}elastic-agent-input-configuration.html`,
policySecrets: `${FLEET_DOCS}agent-policy.html#agent-policy-secret-values`,
remoteESOoutput: `${FLEET_DOCS}monitor-elastic-agent.html#external-elasticsearch-monitoring`,
remoteESOoutput: `${FLEET_DOCS}remote-elasticsearch-output.html`,
performancePresets: `${FLEET_DOCS}es-output-settings.html#es-output-settings-performance-tuning-settings`,
scalingKubernetesResourcesAndLimits: `${FLEET_DOCS}scaling-on-kubernetes.html#_specifying_resources_and_limits_in_agent_manifests`,
roleAndPrivileges: `${FLEET_DOCS}fleet-roles-and-privileges.html`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ describe('EditOutputFlyout', () => {

expect(utils.queryByTestId('serviceTokenSecretInput')).not.toBeNull();

expect(utils.queryByTestId('remoteClusterConfigurationCallout')).not.toBeNull();
expect(utils.queryByTestId('kibanaAPIKeyCallout')).not.toBeNull();
expect(
(utils.getByTestId('settingsOutputsFlyout.kibanaURLInput') as HTMLInputElement).value
Expand All @@ -376,12 +377,21 @@ describe('EditOutputFlyout', () => {
is_default_monitoring: false,
service_token: '1234',
hosts: ['https://localhost:9200'],
kibana_url: 'http://localhost:5601',
kibana_api_key: 'key',
});

expect((utils.getByTestId('serviceTokenSecretInput') as HTMLInputElement).value).toEqual(
'1234'
);

expect(utils.queryByTestId('settingsOutputsFlyout.kibanaURLInput')).toBeNull();
expect(utils.queryByTestId('kibanaAPIKeySecretInput')).toBeNull();

fireEvent.click(utils.getByTestId('syncIntegrationsSwitch'));
expect(
(utils.getByTestId('settingsOutputsFlyout.kibanaURLInput') as HTMLInputElement).value
).toEqual('http://localhost:5601');
expect((utils.getByTestId('kibanaAPIKeySecretInput') as HTMLInputElement).value).toEqual('key');

fireEvent.click(utils.getByText('Save and apply settings'));
Expand All @@ -390,9 +400,11 @@ describe('EditOutputFlyout', () => {
expect(mockSendPutOutput).toHaveBeenCalledWith(
'outputR',
expect.objectContaining({
sync_integrations: true,
secrets: { service_token: '1234', kibana_api_key: 'key' },
service_token: undefined,
kibana_api_key: undefined,
kibana_url: 'http://localhost:5601',
})
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import {
EuiFormRow,
EuiSpacer,
EuiSwitch,
EuiButton,
EuiLink,
EuiCode,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
Expand All @@ -21,6 +24,8 @@ import { MultiRowInput } from '../multi_row_input';

import { ExperimentalFeaturesService } from '../../../../services';

import { useStartServices } from '../../../../hooks';

import type { OutputFormInputsType } from './use_output_form';
import { SecretFormRow } from './output_form_secret_form_row';
import { SSLFormSection } from './ssl_form_section';
Expand All @@ -38,13 +43,16 @@ export interface IsConvertedToSecret {
}

export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props) => {
const { docLinks } = useStartServices();
const { inputs, useSecretsStorage, onToggleSecretStorage } = props;
const [isConvertedToSecret, setIsConvertedToSecret] = React.useState<IsConvertedToSecret>({
serviceToken: false,
kibanaAPIKey: false,
sslKey: false,
});
const { enableSyncIntegrationsOnRemote, enableSSLSecrets } = ExperimentalFeaturesService.get();
const [isRemoteClusterInstructionsOpen, setIsRemoteClusterInstructionsOpen] =
React.useState(false);

const [isFirstLoad, setIsFirstLoad] = React.useState(true);

Expand Down Expand Up @@ -192,7 +200,7 @@ export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props)
title={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.serviceTokenCalloutText"
defaultMessage="Generate a service token by running this API request in the Remote Kibana Console and copy the response value"
defaultMessage="Generate a service token by running this API request in the remote Kibana Console and copy the response value"
/>
}
data-test-subj="serviceTokenCallout"
Expand All @@ -212,13 +220,14 @@ export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props)
helpText={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.syncIntegrationsFormRowLabel"
defaultMessage="If enabled, Integration assets will be installed on the remote Elasticsearch cluster"
defaultMessage="If enabled, integration assets will be installed on the remote Elasticsearch cluster"
/>
}
{...inputs.syncIntegrationsInput.formRowProps}
>
<EuiSwitch
{...inputs.syncIntegrationsInput.props}
data-test-subj="syncIntegrationsSwitch"
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.syncIntegrationsSwitchLabel"
Expand All @@ -227,91 +236,190 @@ export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props)
}
/>
</EuiFormRow>
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.kibanaURLInputLabel"
defaultMessage="Remote Kibana URL"
/>
}
{...inputs.kibanaURLInput.formRowProps}
>
<EuiFieldText
data-test-subj="settingsOutputsFlyout.kibanaURLInput"
fullWidth
{...inputs.kibanaURLInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.kibanaURLInputPlaceholder',
{
defaultMessage: 'Specify Kibana URL',
{inputs.syncIntegrationsInput.value === true && (
<>
<EuiSpacer size="m" />
<EuiCallOut
iconType="iInCircle"
title={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.remoteClusterConfigurationCalloutTitle"
defaultMessage="Additional remote cluster configuration required"
/>
}
)}
/>
</EuiFormRow>
<EuiSpacer size="m" />
{!useSecretsStorage ? (
<SecretFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyLabel"
defaultMessage="Remote Kibana API Key"
/>
}
{...inputs.kibanaAPIKeyInput.formRowProps}
useSecretsStorage={useSecretsStorage}
onToggleSecretStorage={onToggleSecretAndClearValue}
>
<EuiFieldText
fullWidth
data-test-subj="kibanaAPIKeySecretInput"
{...inputs.kibanaAPIKeyInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyPlaceholder',
{
defaultMessage: 'Specify Kibana API Key',
}
data-test-subj="remoteClusterConfigurationCallout"
>
{isRemoteClusterInstructionsOpen ? (
<EuiButton onClick={() => setIsRemoteClusterInstructionsOpen(false)}>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.collapseInstructionsButtonLabel"
defaultMessage="Collapse steps"
/>
</EuiButton>
) : (
<EuiButton onClick={() => setIsRemoteClusterInstructionsOpen(true)} fill={true}>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.viewInstructionButtonLabel"
defaultMessage="View steps"
/>
</EuiButton>
)}
/>
</SecretFormRow>
) : (
<SecretFormRow
fullWidth
title={i18n.translate('xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyLabel', {
defaultMessage: 'Remote Kibana API Key',
})}
{...inputs.kibanaAPIKeySecretInput.formRowProps}
cancelEdit={inputs.kibanaAPIKeySecretInput.cancelEdit}
useSecretsStorage={useSecretsStorage}
isConvertedToSecret={isConvertedToSecret.kibanaAPIKey}
onToggleSecretStorage={onToggleSecretAndClearValue}
>
<EuiFieldText
data-test-subj="kibanaAPIKeySecretInput"
{isRemoteClusterInstructionsOpen && (
<>
<EuiSpacer size="m" />
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.description"
defaultMessage="To sync integrations from this cluster, the remote Elasticsearch output needs additional configuration. {documentationLink}."
values={{
documentationLink: (
<EuiLink external={true} href={docLinks.links.fleet.remoteESOoutput}>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.documentationLink"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
<EuiSpacer size="m" />
<ol>
<li>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.addRemoteClusterStep"
defaultMessage="In the remote cluster, open Kibana and go to {appPath}, and follow the steps to add this cluster."
values={{
appPath: (
<strong>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.addRemoteClusterKibanaPath"
defaultMessage="Stack Management > Remote Clusters"
/>
</strong>
),
}}
/>
<EuiSpacer size="s" />
</li>
<li>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.replicationStep"
defaultMessage="Go to {appPath} and create a follower index using the cluster from Step 1. The leader index {leaderIndex} from this cluster and should be replicated to the follower index {followerIndex} on the remote cluster."
values={{
appPath: (
<strong>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.replicationKibanaPath"
defaultMessage="Stack Management > Cross-Cluster Replication"
/>
</strong>
),
leaderIndex: <EuiCode>fleet-synced-integrations</EuiCode>,
followerIndex: (
<EuiCode>
fleet-synced-integrations-ccr-
{inputs.nameInput.props.value || '<output name>'}
</EuiCode>
),
}}
/>
<EuiSpacer size="s" />
</li>
<li>
<FormattedMessage
id="xpack.fleet.settings.remoteClusterConfiguration.configureKibanaStep"
defaultMessage="Below, provide the access details for the remote cluster's Kibana instance."
/>
</li>
</ol>
</>
)}
</EuiCallOut>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
{...inputs.kibanaAPIKeySecretInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyPlaceholder',
{
defaultMessage: 'Specify Kibana API Key',
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.kibanaURLInputLabel"
defaultMessage="Remote Kibana URL"
/>
}
{...inputs.kibanaURLInput.formRowProps}
>
<EuiFieldText
data-test-subj="settingsOutputsFlyout.kibanaURLInput"
fullWidth
{...inputs.kibanaURLInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.kibanaURLInputPlaceholder',
{
defaultMessage: 'Specify Kibana URL',
}
)}
/>
</EuiFormRow>
<EuiSpacer size="m" />
{!useSecretsStorage ? (
<SecretFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyLabel"
defaultMessage="Remote Kibana API Key"
/>
}
)}
/>
</SecretFormRow>
)}
<EuiSpacer size="m" />
<EuiCallOut
title={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyCalloutText"
defaultMessage="Create an API Key by running this API request in the Remote Kibana Console and copy the encoded value"
/>
}
data-test-subj="kibanaAPIKeyCallout"
>
<EuiCodeBlock isCopyable={true}>
{` POST /_security/api_key
{...inputs.kibanaAPIKeyInput.formRowProps}
useSecretsStorage={useSecretsStorage}
onToggleSecretStorage={onToggleSecretAndClearValue}
>
<EuiFieldText
fullWidth
data-test-subj="kibanaAPIKeySecretInput"
{...inputs.kibanaAPIKeyInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyPlaceholder',
{
defaultMessage: 'Specify Kibana API Key',
}
)}
/>
</SecretFormRow>
) : (
<SecretFormRow
fullWidth
title={i18n.translate('xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyLabel', {
defaultMessage: 'Remote Kibana API Key',
})}
{...inputs.kibanaAPIKeySecretInput.formRowProps}
cancelEdit={inputs.kibanaAPIKeySecretInput.cancelEdit}
useSecretsStorage={useSecretsStorage}
isConvertedToSecret={isConvertedToSecret.kibanaAPIKey}
onToggleSecretStorage={onToggleSecretAndClearValue}
>
<EuiFieldText
data-test-subj="kibanaAPIKeySecretInput"
fullWidth
{...inputs.kibanaAPIKeySecretInput.props}
placeholder={i18n.translate(
'xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyPlaceholder',
{
defaultMessage: 'Specify Kibana API Key',
}
)}
/>
</SecretFormRow>
)}
<EuiSpacer size="m" />
<EuiCallOut
title={
<FormattedMessage
id="xpack.fleet.settings.editOutputFlyout.kibanaAPIKeyCalloutText"
defaultMessage="Create an API Key by running this API request in the remote Kibana Console and copy the encoded value"
/>
}
data-test-subj="kibanaAPIKeyCallout"
>
<EuiCodeBlock isCopyable={true}>
{` POST /_security/api_key
{
"name": "integration_sync_api_key",
"role_descriptors": {
Expand All @@ -326,9 +434,11 @@ export const OutputFormRemoteEsSection: React.FunctionComponent<Props> = (props)
}
}
}`}
</EuiCodeBlock>
</EuiCallOut>
<EuiSpacer size="m" />
</EuiCodeBlock>
</EuiCallOut>
<EuiSpacer size="m" />
</>
)}
</>
) : null}
</>
Expand Down

0 comments on commit f0797db

Please sign in to comment.