Skip to content

Commit

Permalink
[Fleet] add setup technology selector to add integration page (#189612)
Browse files Browse the repository at this point in the history
## Summary

Closes #183863

TODO refactor cspm integration so that the same setup technology
selector is used as other packages
I might do this in a separate pr, it seems this pr also adds Beta badge
to the cspm selector #189217

Note: in Serverless, currently the preconfigured `agentless` agent
policy is being used for all agentless integrations. When Agentless API
is supported in serverless, this code can be removed and serverless will
work the same way as ESS. Related issue
elastic/security-team#9781

Open question:
- Do we want to allow preconfigured agentless agent policies in ESS?
Currently it's only allowed in serverless:
https://github.com/elastic/kibana/blob/main/x-pack/plugins/fleet/server/services/preconfiguration.ts#L167-L176

Requirements completed:
- [x] Add a agentless/agent-based selector component when
`deployment_modes.agentless.enabled: true` is set on a given policy
template
- [x] Hide/show the "Select or create agent policy" UI based on the
state of the agentless selector
- [x] Always create a new agent policy when saving an integration policy
in agentless mode. This newly created policy should have
`supports_agentless: true` set.
- [x] When deleting an agentless integration policy, the agentless agent
policy should also be deleted
- [x] The toggle and agentless logic should still be gated behind the
agentless feature flag in addition to the integration level
configuration
- [x] Ensure that even if `supports_agentless: true` is set, the policy
editor default to the current agent-based option and the toggle is set
to `agent-based`
- [x] Ensure that the `agentless` option is clearly marked as beta, and
display a beta banner in the policy editor form itself to reiterate this
when agentless mode is enabled
- [x] Editing an agentless integration policy should not be possible.
The "edit" action should be disabled with a tooltip directing the user
to instead create a new integration policy and delete the existing one.

## Steps to verify locally (ESS):
- Add to `kibana.dev.yml`:
```
xpack.fleet.enableExperimental: ['agentless'] 
xpack.fleet.agentless.api.url: 'https://api.agentless.url/api/v1/ess'
xpack.fleet.agentless.api.tls.certificate: './config/node.crt'
xpack.fleet.agentless.api.tls.key: './config/node.key'
xpack.fleet.agentless.api.tls.ca: './config/ca.crt'
xpack.cloud.id: '123456789'
```
- Upload agentless package
[agentless_test_package-0.0.1.zip](https://github.com/user-attachments/files/16439462/agentless_test_package-0.0.1.zip)
```
curl -XPOST -H 'content-type: application/zip' -H 'kbn-xsrf: true' http://localhost:5601/julia/api/fleet/epm/packages -u elastic:changeme --data-binary @agentless_test_package-0.0.1.zip
```
Verify Agent-based option:
- Go to Add integration - Agentless test package
- Setup technology selector should be visible, `Agent-based` option
selected by default
- New/Existing agent policy selection should be visible
- On submitting, the Agentless test package integration policy should be
created with the selected agent policies as normal
- Edit integration should be enabled as normal

<img width="1137" alt="image"
src="https://github.com/user-attachments/assets/182b89eb-f133-41e9-8671-70ef0d70b8c7">
<img width="1140" alt="image"
src="https://github.com/user-attachments/assets/f9b6f676-7078-457a-8b50-8c68ffc9d31d">

Verify Agentless option:
- To verify the agentless option, add another integration
- Select `Agentless` option in the selector
- New/Existing agent policy selection should be hidden
- On submitting, the Agentless test package integration policy should be
created with a new managed agent policy
**Note:** locally the agentless API won't work, comment out this line to
test
https://github.com/elastic/kibana/blob/main/x-pack/plugins/fleet/server/services/agent_policy_create.ts#L178
- Edit integration should be disabled
- Delete integration action should be enabled, when clicking it, the
agent policy should be deleted too, navigating back to the list of agent
policies

<img width="985" alt="image"
src="https://github.com/user-attachments/assets/39b5fe9f-5b6d-4b76-9c9e-ff0b7e5414a6">
<img width="1147" alt="image"
src="https://github.com/user-attachments/assets/fa76d092-cdfb-488b-ab68-c7ec436c20c5">
<img width="1136" alt="image"
src="https://github.com/user-attachments/assets/8a6ef1da-7d9c-42ce-9de8-cc5c64c079d0">
<img width="1139" alt="image"
src="https://github.com/user-attachments/assets/5e38388d-8219-4aef-8d85-6264a6a5fa9f">
<img width="1138" alt="image"
src="https://github.com/user-attachments/assets/827884ec-56d0-4225-bf2f-f0aeb2fcb35d">


### Checklist

- [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/packages/kbn-i18n/README.md)
- [x] [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: David Kilfoyle <41695641+kilfoyle@users.noreply.github.com>
  • Loading branch information
juliaElastic and kilfoyle authored Aug 2, 2024
1 parent 008e70d commit e67b460
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 39 deletions.
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

0 comments on commit e67b460

Please sign in to comment.