Skip to content

Commit

Permalink
[Fleet] Create shared package policy (elastic#185916)
Browse files Browse the repository at this point in the history
## Summary

Closes elastic/ingest-dev#3263

Added multi-select agent policy component to Add integration policy
page.

## Steps to verify

To verify:
- Enable feature flag by adding `xpack.fleet.enableExperimental:
['enableReusableIntegrationPolicies']` to `kibana.dev.yml` locally
- Create a few agent policies
- Open Integrations UI, click on Add integration
- Go to Existing hosts tab at the bottom
- Verify that the agent policy selector support multiple selection
- Verify that all selected agent policy ids are included in the API
request (check with `Preview API Request` button)

<img width="870" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/57649305-6be1-424d-a5be-08fabbfc475b">

## Logstash output restriction

There are a few scenarios where agent policy selection is restricted.
- Add logstash output to one of the agent policies
- Try to add APM integration policy
- Verify that agent policy with logstash output is disabled
- The new `EuiComboBox` component doesn't support a multiline custom
option rendering, so added a warning icon with a tooltip instead.

<img width="889" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/259463fa-9831-4bdc-a8b7-edfd8efbd18d">

Existing UI with `EuiSuperSelect`:

<img width="781" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/67633284-9c89-4509-b80b-5710c7b22a63">

## Limited packages restriction

The other restrictions don't allow limited packages to be added twice to
an agent policy: endpoint, osquery.
Test by adding osquery_manager to one agent policy, and then try to add
another integration policy to the same agent policy. It should be
disabled.

<img width="871" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/50035517-cbe5-439b-bd10-ca5a8f600ba8">

## Agent policies used by agents

The UI shows the sum of agents enrolled to the selected agent policies

<img width="874" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/53cc760a-896a-4ae3-a588-86debff3af23">

The `Preview API Request` feature should keep the `policy_ids` list in
sync when adding/removing agent policies.

<img width="731" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/131bca40-70e3-44eb-9bbd-3563646bb597">

The confirmation modal should also show a sum of enrolled agents and
list of selected agent policies.

<img width="779" alt="image"
src="https://github.com/elastic/kibana/assets/90178898/d6453006-18fd-463f-8ce4-cc18c142e265">



### 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: Kyle Pollich <kpollich1@gmail.com>
  • Loading branch information
juliaElastic and kpollich committed Jun 12, 2024
1 parent 9b4bd89 commit 74738c0
Show file tree
Hide file tree
Showing 21 changed files with 688 additions and 328 deletions.
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/experimental_features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const allowedExperimentalValues = Object.freeze<Record<string, boolean>>(
enablePackagesStateMachine: true,
advancedPolicySettings: true,
useSpaceAwareness: false,
enableReusableIntegrationPolicies: false,
});

type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
onConfirm: () => void;
onCancel: () => void;
agentCount: number;
agentPolicy: AgentPolicy;
agentPolicies: AgentPolicy[];
showUnprivilegedAgentsCallout?: boolean;
unprivilegedAgentsCount?: number;
dataStreams?: Array<{ name: string; title: string }>;
}> = ({
onConfirm,
onCancel,
agentCount,
agentPolicy,
agentPolicies,
showUnprivilegedAgentsCallout = false,
unprivilegedAgentsCount = 0,
dataStreams,
Expand Down Expand Up @@ -66,11 +66,11 @@ export const ConfirmDeployAgentPolicyModal: React.FunctionComponent<{
<div className="eui-textBreakWord">
<FormattedMessage
id="xpack.fleet.agentPolicy.confirmModalCalloutDescription"
defaultMessage="Fleet has detected that the selected agent policy, {policyName}, is already in use by
defaultMessage="Fleet has detected that the selected agent policies, {policyNames}, are already in use by
some of your agents. As a result of this action, Fleet will deploy updates to all agents
that use this policy."
that use these policies."
values={{
policyName: <b>{agentPolicy.name}</b>,
policyNames: <b>{agentPolicies.map((policy) => policy.name).join(', ')}</b>,
}}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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 type { EuiComboBoxOptionOption } from '@elastic/eui';
import { EuiComboBox } from '@elastic/eui';

import { i18n } from '@kbn/i18n';

import React, { useMemo } from 'react';

import type { PackageInfo } from '../../../../../../../../../common';

export interface Props {
isLoading: boolean;
agentPolicyMultiOptions: Array<EuiComboBoxOptionOption<string>>;
selectedPolicyIds: string[];
setSelectedPolicyIds: (policyIds: string[]) => void;
packageInfo?: PackageInfo;
}

export const AgentPolicyMultiSelect: React.FunctionComponent<Props> = ({
isLoading,
agentPolicyMultiOptions,
selectedPolicyIds,
setSelectedPolicyIds,
}) => {
const selectedOptions = useMemo(() => {
return agentPolicyMultiOptions.filter((option) => selectedPolicyIds.includes(option.key!));
}, [agentPolicyMultiOptions, selectedPolicyIds]);

return (
<EuiComboBox
aria-label="Select Multiple Agent Policies"
data-test-subj="agentPolicyMultiSelect"
placeholder={i18n.translate(
'xpack.fleet.createPackagePolicy.StepSelectPolicy.agentPolicyMultiPlaceholderText',
{
defaultMessage: 'Select agent policies to add this integration to',
}
)}
options={agentPolicyMultiOptions}
selectedOptions={selectedOptions}
onChange={(newOptions) => {
setSelectedPolicyIds(newOptions.map((option: any) => option.key));
}}
isClearable={true}
isLoading={isLoading}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,8 @@ import type { TestRenderer } from '../../../../../../../mock';
import { createFleetTestRendererMock } from '../../../../../../../mock';
import type { AgentPolicy, NewPackagePolicy, PackageInfo } from '../../../../../types';

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

import { StepDefinePackagePolicy } from './step_define_package_policy';

jest.mock('../../../../../hooks', () => {
return {
...jest.requireActual('../../../../../hooks'),
useGetPackagePolicies: jest.fn().mockReturnValue({
data: {
items: [{ name: 'nginx-1' }, { name: 'other-policy' }],
},
isLoading: false,
}),
useFleetStatus: jest.fn().mockReturnValue({ isReady: true } as any),
sendGetStatus: jest
.fn()
.mockResolvedValue({ data: { isReady: true, missing_requirements: [] } }),
};
});

describe('StepDefinePackagePolicy', () => {
const packageInfo: PackageInfo = {
name: 'apache',
Expand Down Expand Up @@ -63,18 +45,32 @@ describe('StepDefinePackagePolicy', () => {
},
],
};
const agentPolicy: AgentPolicy = {
id: 'agent-policy-1',
namespace: 'ns',
name: 'Agent policy 1',
is_managed: false,
status: 'active',
updated_at: '',
updated_by: '',
revision: 1,
package_policies: [],
is_protected: false,
};
const agentPolicies: AgentPolicy[] = [
{
id: 'agent-policy-1',
namespace: 'ns',
name: 'Agent policy 1',
is_managed: false,
status: 'active',
updated_at: '',
updated_by: '',
revision: 1,
package_policies: [],
is_protected: false,
},
{
id: 'agent-policy-2',
namespace: 'default',
name: 'Agent policy 2',
is_managed: false,
status: 'active',
updated_at: '',
updated_by: '',
revision: 1,
package_policies: [],
is_protected: false,
},
];
let packagePolicy: NewPackagePolicy;
const mockUpdatePackagePolicy = jest.fn().mockImplementation((val: any) => {
packagePolicy = {
Expand All @@ -96,7 +92,7 @@ describe('StepDefinePackagePolicy', () => {
const render = () =>
(renderResult = testRenderer.render(
<StepDefinePackagePolicy
agentPolicy={agentPolicy}
agentPolicies={agentPolicies}
packageInfo={packageInfo}
packagePolicy={packagePolicy}
updatePackagePolicy={mockUpdatePackagePolicy}
Expand Down Expand Up @@ -139,30 +135,14 @@ describe('StepDefinePackagePolicy', () => {

waitFor(() => {
expect(renderResult.getByRole('switch')).toHaveAttribute('aria-label', 'Advanced var');
expect(renderResult.getByTestId('packagePolicyNamespaceInput')).toHaveAttribute(
'placeholder',
'ns'
);
});
});
});

it('should set incremented name if other package policies exist', () => {
(useGetPackagePolicies as jest.MockedFunction<any>).mockReturnValueOnce({
data: {
items: [
{ name: 'apache-1' },
{ name: 'apache-2' },
{ name: 'apache-9' },
{ name: 'apache-10' },
],
},
isLoading: false,
});

render();

waitFor(() => {
expect(renderResult.getByDisplayValue('apache-11')).toBeInTheDocument();
});
});

describe('update', () => {
describe('when package vars are introduced in a new package version', () => {
it('should display new package vars', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const FormGroupResponsiveFields = styled(EuiDescribedFormGroup)`
`;

export const StepDefinePackagePolicy: React.FunctionComponent<{
agentPolicy?: AgentPolicy;
agentPolicies?: AgentPolicy[];
packageInfo: PackageInfo;
packagePolicy: NewPackagePolicy;
updatePackagePolicy: (fields: Partial<NewPackagePolicy>) => void;
Expand All @@ -58,7 +58,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
noAdvancedToggle?: boolean;
}> = memo(
({
agentPolicy,
agentPolicies,
packageInfo,
packagePolicy,
updatePackagePolicy,
Expand Down Expand Up @@ -283,8 +283,9 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
}
>
<EuiComboBox
data-test-subj="packagePolicyNamespaceInput"
noSuggestions
placeholder={agentPolicy?.namespace}
placeholder={agentPolicies?.[0]?.namespace}
isDisabled={isEditPage && packageInfo.type === 'input'}
singleSelection={true}
selectedOptions={
Expand Down
Loading

0 comments on commit 74738c0

Please sign in to comment.