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

[Fleet] add setup technology selector to add integration page #189612

Merged
merged 10 commits into from
Aug 2, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';

import { FormattedMessage } from '@kbn/i18n-react';
import { EuiBetaBadge, EuiFormRow, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui';

import { SetupTechnology } from '../../../../../types';

export const SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ = 'setup-technology-selector';

export const SetupTechnologySelector = ({
disabled,
setupTechnology,
onSetupTechnologyChange,
}: {
disabled: boolean;
setupTechnology: SetupTechnology;
onSetupTechnologyChange: (value: SetupTechnology) => void;
}) => {
const options = [
{
value: SetupTechnology.AGENTLESS,
inputDisplay: (
<>
<FormattedMessage
id="xpack.fleet.setupTechnology.agentlessInputDisplay"
defaultMessage="Agentless"
/>
&nbsp;
<EuiBetaBadge
label="Beta"
size="s"
tooltipContent="This module is not yet GA. Please help us by reporting any bugs."
/>
</>
),
dropdownDisplay: (
<>
<strong>
<FormattedMessage
id="xpack.fleet.setupTechnology.agentlessDrowpownDisplay"
defaultMessage="Agentless"
/>
</strong>
&nbsp;
<EuiBetaBadge
label="Beta"
size="s"
tooltipContent="This module is not GA. Please help us by reporting any bugs."
/>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.fleet.setupTechnology.agentlessDrowpownDescription"
defaultMessage="Set up the integration without an agent"
/>
</p>
</EuiText>
</>
),
},
{
value: SetupTechnology.AGENT_BASED,
inputDisplay: (
<FormattedMessage
id="xpack.fleet.setupTechnology.agentbasedInputDisplay"
defaultMessage="Agent-based"
/>
),
dropdownDisplay: (
<>
<strong>
<FormattedMessage
id="xpack.fleet.setupTechnology.agentbasedDrowpownDisplay"
defaultMessage="Agent-based"
/>
</strong>
<EuiText size="s" color="subdued">
<p>
<FormattedMessage
id="xpack.fleet.setupTechnology.agentbasedDrowpownDescription"
defaultMessage="Set up the integration with an agent"
/>
</p>
</EuiText>
</>
),
},
];

return (
<>
<EuiSpacer size="l" />
<EuiFormRow
fullWidth
label={
<FormattedMessage
id="xpack.fleet.setupTechnology.setupTechnologyLabel"
defaultMessage="Setup technology"
/>
}
>
<EuiSuperSelect
disabled={disabled}
options={options}
valueOfSelected={setupTechnology}
placeholder={
<FormattedMessage
id="xpack.fleet.setupTechnology.setupTechnologyPlaceholder"
defaultMessage="Select the setup technology"
/>
}
onChange={onSetupTechnologyChange}
itemLayoutAlign="top"
hasDividers
fullWidth
data-test-subj={SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ}
/>
</EuiFormRow>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,8 @@ export function useSetupTechnology({
isEditPage?: boolean;
}) {
const { cloud } = useStartServices();
const {
isAgentlessEnabled,
isAgentlessIntegration,
isAgentlessCloudEnabled,
isAgentlessServerlessEnabled,
} = useAgentless();
const { isAgentlessEnabled, isAgentlessCloudEnabled, isAgentlessServerlessEnabled } =
useAgentless();

// this is a placeholder for the new agent-BASED policy that will be used when the user switches from agentless to agent-based and back
const newAgentBasedPolicy = useRef<NewAgentPolicy>(newAgentPolicy);
Expand All @@ -107,6 +103,7 @@ export function useSetupTechnology({
const [newAgentlessPolicy, setNewAgentlessPolicy] = useState<AgentPolicy | NewAgentPolicy>(
generateNewAgentPolicyWithDefaults({
supports_agentless: true,
monitoring_enabled: [],
})
);

Expand Down Expand Up @@ -135,12 +132,6 @@ export function useSetupTechnology({
setNewAgentPolicy,
]);

useEffect(() => {
if (isAgentlessEnabled && packageInfo && isAgentlessIntegration(packageInfo)) {
setSelectedSetupTechnology(SetupTechnology.AGENTLESS);
}
}, [isAgentlessEnabled, isAgentlessIntegration, packageInfo]);

// tech debt: remove this useEffect when Serverless uses the Agentless API
// https://github.com/elastic/security-team/issues/9781
useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ jest.mock('react-router-dom', () => ({
import { AGENTLESS_POLICY_ID } from '../../../../../../../common/constants';

import { CreatePackagePolicySinglePage } from '.';
import { SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ } from './components/setup_technology_selector';

// mock console.debug to prevent noisy logs from console.debugs in ./index.tsx
let consoleDebugMock: any;
Expand Down Expand Up @@ -160,6 +161,7 @@ describe('When on the package policy create page', () => {
function getMockPackageInfo(options?: {
requiresRoot?: boolean;
dataStreamRequiresRoot?: boolean;
agentlessEnabled?: boolean;
}) {
return {
data: {
Expand All @@ -181,6 +183,7 @@ describe('When on the package policy create page', () => {
},
],
multiple: true,
deployment_modes: { agentless: { enabled: options?.agentlessEnabled } },
},
],
data_streams: [
Expand Down Expand Up @@ -802,23 +805,54 @@ describe('When on the package policy create page', () => {
});
jest.spyOn(ExperimentalFeaturesService, 'get').mockReturnValue({ agentless: true } as any);
(useGetPackageInfoByKeyQuery as jest.Mock).mockReturnValue(
getMockPackageInfo({ requiresRoot: false, dataStreamRequiresRoot: false })
getMockPackageInfo({
requiresRoot: false,
dataStreamRequiresRoot: false,
agentlessEnabled: true,
})
);

await act(async () => {
render();
});
});

test('should create create agent and package policy when in cloud and agentless API url is set', async () => {
test('should create agent policy and package policy when in cloud and agentless API url is set', async () => {
await act(async () => {
fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
});

// tech debt: this should be converted to use MSW to mock the API calls
// https://github.com/elastic/security-team/issues/9816
expect(sendGetOneAgentPolicy).not.toHaveBeenCalled();
expect(sendCreateAgentPolicy).toHaveBeenCalled();
expect(sendCreateAgentPolicy).toHaveBeenCalledWith(
expect.objectContaining({
monitoring_enabled: ['logs', 'metrics'],
name: 'Agent policy 1',
}),
{ withSysMonitoring: true }
);
expect(sendCreatePackagePolicy).toHaveBeenCalled();

await waitFor(() => {
expect(renderResult.getByText('Nginx integration added')).toBeInTheDocument();
});
});

test('should create agentless agent policy and package policy when in cloud and agentless API url is set', async () => {
fireEvent.click(renderResult.getByTestId(SETUP_TECHNOLOGY_SELECTOR_TEST_SUBJ));
fireEvent.click(renderResult.getByText('Agentless'));
await act(async () => {
fireEvent.click(renderResult.getByText(/Save and continue/).closest('button')!);
});

expect(sendCreateAgentPolicy).toHaveBeenCalledWith(
expect.objectContaining({
monitoring_enabled: [],
name: 'Agentless policy for nginx-1',
supports_agentless: true,
}),
{ withSysMonitoring: false }
);
expect(sendCreatePackagePolicy).toHaveBeenCalled();

await waitFor(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { PostInstallGoogleCloudShellModal } from './components/cloud_security_po
import { PostInstallAzureArmTemplateModal } from './components/cloud_security_posture/post_install_azure_arm_template_modal';
import { RootPrivilegesCallout } from './root_callout';
import { useAgentless } from './hooks/setup_technology';
import { SetupTechnologySelector } from './components/setup_technology_selector';

export const StepsWithLessPadding = styled(EuiSteps)`
.euiStep__content {
Expand Down Expand Up @@ -349,7 +350,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
"'package-policy-create' and 'package-policy-replace-define-step' cannot both be registered as UI extensions"
);
}
const { isAgentlessEnabled } = useAgentless();
const { isAgentlessEnabled, isAgentlessIntegration } = useAgentless();
const { handleSetupTechnologyChange, selectedSetupTechnology } = useSetupTechnology({
newAgentPolicy,
setNewAgentPolicy,
Expand Down Expand Up @@ -397,6 +398,19 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
submitAttempted={formState === 'INVALID'}
/>

{/* TODO move SetupTechnologySelector out of extensionView */}
{!extensionView && isAgentlessIntegration(packageInfo) && (
<SetupTechnologySelector
disabled={false}
setupTechnology={selectedSetupTechnology}
onSetupTechnologyChange={(value) => {
handleSetupTechnologyChange(value);
// agentless doesn't need system integration
setWithSysMonitoring(value === SetupTechnology.AGENT_BASED);
}}
/>
)}

{/* Only show the out-of-box configuration step if a UI extension is NOT registered */}
{!extensionView && (
<StepConfigurePackagePolicy
Expand Down Expand Up @@ -435,6 +449,9 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
extensionView,
handleExtensionViewOnChange,
spaceSettings?.allowedNamespacePrefixes,
handleSetupTechnologyChange,
isAgentlessIntegration,
selectedSetupTechnology,
]
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,18 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
<EuiFlexGroup gutterSize="s" alignItems="center">
<EuiFlexItem data-test-subj="PackagePoliciesTableName" grow={false}>
<EuiLink
title={value}
{...(canReadIntegrationPolicies
title={
agentPolicy.supports_agentless
? i18n.translate(
'xpack.fleet.policyDetails.packagePoliciesTable.disabledEditTitle',
{
defaultMessage:
'Editing an agentless integration is not supported. Add a new integration if needed.',
}
)
: value
}
{...(canReadIntegrationPolicies && !agentPolicy.supports_agentless
? {
href: getHref('edit_integration', {
policyId: agentPolicy.id,
Expand All @@ -129,9 +139,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
}
: { disabled: true })}
>
<span className="eui-textTruncate" title={value}>
{value}
</span>
<span className="eui-textTruncate">{value}</span>
{packagePolicy.description ? (
<span>
&nbsp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,31 @@ interface InMemoryPackagePolicyAndAgentPolicy {

const IntegrationDetailsLink = memo<{
packagePolicy: InMemoryPackagePolicyAndAgentPolicy['packagePolicy'];
}>(({ packagePolicy }) => {
agentPolicies: InMemoryPackagePolicyAndAgentPolicy['agentPolicies'];
}>(({ packagePolicy, agentPolicies }) => {
const { getHref } = useLink();
const policySupportsAgentless = agentPolicies?.some((policy) => policy.supports_agentless);
return (
<EuiLink
className="eui-textTruncate"
data-test-subj="integrationNameLink"
title={packagePolicy.name}
href={getHref('integration_policy_edit', {
packagePolicyId: packagePolicy.id,
})}
{...(policySupportsAgentless
? {
disabled: true,
title: i18n.translate(
'xpack.fleet.epm.packageDetails.integrationList.disabledEditTitle',
{
defaultMessage:
'Editing an agentless integration is not supported. Add a new integration if needed.',
}
),
}
: {
href: getHref('integration_policy_edit', {
packagePolicyId: packagePolicy.id,
}),
title: packagePolicy.name,
})}
>
{packagePolicy.name}
</EuiLink>
Expand Down Expand Up @@ -182,8 +197,10 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps
name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', {
defaultMessage: 'Integration policy',
}),
render(_, { packagePolicy }) {
return <IntegrationDetailsLink packagePolicy={packagePolicy} />;
render(_, { agentPolicies, packagePolicy }) {
return (
<IntegrationDetailsLink packagePolicy={packagePolicy} agentPolicies={agentPolicies} />
);
},
},
{
Expand Down
Loading
Loading