Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[UII] Add remote cluster instructions for syncing integrations #211997

Merged
merged 5 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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}>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kilfoyle I expect we'll file a docs request when #187323 is near completion and I think it probably makes sense for the new content to live on the same remote ES output docs page, but we can update the link here if not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup! That makes sense to me. Best to keep all the remote ES stuff together on the same page.

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