Skip to content

Commit 8266415

Browse files
[UII] Support integration-level outputs (elastic#189125)
## Summary Resolves elastic#143905. This PR adds support for integration-level outputs. This means that different integrations within the same agent policy can now be configured to send data to different locations. This feature is gated behind `enterprise` level subscription. For each input, the agent policy will configure sending data to the following outputs in decreasing order of priority: 1. Output set specifically on the integration policy 2. Output set specifically on the integration's parent agent policy (including the case where an integration policy belongs to multiple agent policies) 3. Global default data output set via Fleet Settings Integration-level outputs will respect the same rules as agent policy-level outputs: - Certain integrations are disallowed from using certain output types, attempting to add them to each other via creation, updating, or "defaulting", will fail - `fleet-server`, `synthetics`, and `apm` can only use same-cluster Elasticsearch output - When an output is deleted, any integrations that were specifically using it will "clear" their output configuration and revert back to either `#2` or `#3` in the above list - When an output is edited, all agent policies across all spaces that use it will be bumped to a new revision, this includes: - Agent policies that have that output specifically set in their settings (existing behavior) - Agent policies that contain integrations which specifically has that output set (new behavior) - When a proxy is edited, the same new revision bump above will apply for any outputs using that proxy The final agent policy YAML that is generated will have: - `outputs` block that includes: - Data and monitoring outputs set at the agent policy level (existing behavior) - Any additional outputs set at the integration level, if they differ from the above - `outputs_permissions` block that includes permissions for each Elasticsearch output depending on which integrations and/or agent monitoring are assigned to it Integration policies table now includes `Output` column. If the output is defaulting to agent policy-level output, or global setting output, a tooltip is shown: <img width="1392" alt="image" src="https://github.com/user-attachments/assets/5534716b-49b5-402a-aa4a-4ba6533e0ca8"> Configuring an integration-level output is done under Advanced options in the policy editor. Setting to the blank value will "clear" the output configuration. The list of available outputs is filtered by what outputs are available for that integration (see above): <img width="799" alt="image" src="https://github.com/user-attachments/assets/617af6f4-e8f8-40b1-b476-848f8ac96e76"> An example of failure: ES output cannot be changed to Kafka while there is an integration <img width="1289" alt="image" src="https://github.com/user-attachments/assets/11847eb5-fd5d-4271-8464-983d7ab39218"> ## TODO - [x] Adjust side effects of editing/deleting output when policies use it across different spaces - [x] Add API integration tests - [x] Update OpenAPI spec - [x] Create doc issue ### Checklist Delete any items that are not applicable to this PR. - [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) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [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: kibanamachine <[email protected]>
1 parent 8ee0493 commit 8266415

File tree

53 files changed

+1266
-279
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1266
-279
lines changed

packages/kbn-check-mappings-update-cli/current_fields.json

+1
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@
630630
"is_managed",
631631
"name",
632632
"namespace",
633+
"output_id",
633634
"overrides",
634635
"package",
635636
"package.name",

packages/kbn-check-mappings-update-cli/current_mappings.json

+3
Original file line numberDiff line numberDiff line change
@@ -2101,6 +2101,9 @@
21012101
"namespace": {
21022102
"type": "keyword"
21032103
},
2104+
"output_id": {
2105+
"type": "keyword"
2106+
},
21042107
"overrides": {
21052108
"index": false,
21062109
"type": "flattened"

src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
119119
"ingest-agent-policies": "90625b4a5ded9d4867358fcccc14a57c0454fcee",
120120
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
121121
"ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91",
122-
"ingest-package-policies": "2c0f7c72d211bb7d3076ce2fc0bd368f9c16d274",
122+
"ingest-package-policies": "53a94064674835fdb35e5186233bcd7052eabd22",
123123
"ingest_manager_settings": "91445219e7115ff0c45d1dabd5d614a80b421797",
124124
"inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83",
125125
"kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad",

x-pack/plugins/fleet/common/constants/mappings.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
/**
9-
* ATTENTION: Mappings for Fleet are defined in the ElasticSearch repo.
9+
* ATTENTION: Mappings for Fleet are defined in the Elasticsearch repo.
1010
*
1111
* The following mappings declared here closely mirror them
1212
* But they are only used to perform validation on the endpoints using ListWithKuery
@@ -54,6 +54,7 @@ export const PACKAGE_POLICIES_MAPPINGS = {
5454
is_managed: { type: 'boolean' },
5555
policy_id: { type: 'keyword' },
5656
policy_ids: { type: 'keyword' },
57+
output_id: { type: 'keyword' },
5758
package: {
5859
properties: {
5960
name: { type: 'keyword' },

x-pack/plugins/fleet/common/constants/output.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const DEFAULT_OUTPUT: NewOutput = {
2929
export const SERVERLESS_DEFAULT_OUTPUT_ID = 'es-default-output';
3030

3131
export const LICENCE_FOR_PER_POLICY_OUTPUT = 'platinum';
32+
export const LICENCE_FOR_OUTPUT_PER_INTEGRATION = 'enterprise';
3233

3334
/**
3435
* Kafka constants

x-pack/plugins/fleet/common/constants/package_policy.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ export const inputsFormat = {
1313
Simplified: 'simplified',
1414
Legacy: 'legacy',
1515
} as const;
16+
17+
export const LICENCE_FOR_MULTIPLE_AGENT_POLICIES = 'enterprise';

x-pack/plugins/fleet/common/openapi/bundled.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -7484,8 +7484,7 @@
74847484
"type": "string"
74857485
},
74867486
"output_id": {
7487-
"type": "string",
7488-
"deprecated": true
7487+
"type": "string"
74897488
},
74907489
"inputs": {
74917490
"type": "array",

x-pack/plugins/fleet/common/openapi/bundled.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -4804,7 +4804,6 @@ components:
48044804
type: string
48054805
output_id:
48064806
type: string
4807-
deprecated: true
48084807
inputs:
48094808
type: array
48104809
items:

x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ properties:
2222
type: string
2323
output_id:
2424
type: string
25-
deprecated: true
2625
inputs:
2726
type: array
2827
items:

x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml

-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ properties:
2323
type: string
2424
output_id:
2525
type: string
26-
description: Not supported output can be set at the agent policy level only
27-
deprecated: true
2826
inputs:
2927
type: array
3028
items:

x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/plugins/fleet/common/services/simplified_package_policy_helper.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ describe('toPackagePolicy', () => {
3737
namespace: 'default',
3838
policy_id: 'policy123',
3939
policy_ids: ['policy123'],
40+
output_id: 'output123',
4041
description: 'Test description',
4142
inputs: {
4243
'nginx-logfile': {

x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts

+6
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface SimplifiedPackagePolicy {
4343
id?: string;
4444
policy_id?: string;
4545
policy_ids: string[];
46+
output_id?: string;
4647
namespace: string;
4748
name: string;
4849
description?: string;
@@ -147,6 +148,7 @@ export function simplifiedPackagePolicytoNewPackagePolicy(
147148
const {
148149
policy_id: policyId,
149150
policy_ids: policyIds,
151+
output_id: outputId,
150152
namespace,
151153
name,
152154
description,
@@ -161,6 +163,10 @@ export function simplifiedPackagePolicytoNewPackagePolicy(
161163
description
162164
);
163165

166+
if (outputId) {
167+
packagePolicy.output_id = outputId;
168+
}
169+
164170
if (packagePolicy.package && options?.experimental_data_stream_features) {
165171
packagePolicy.package.experimental_data_stream_features =
166172
options.experimental_data_stream_features;

x-pack/plugins/fleet/common/types/models/package_policy.ts

+2
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ export interface NewPackagePolicy {
8181
/** @deprecated */
8282
policy_id?: string;
8383
policy_ids: string[];
84+
// Nullable to allow user to reset to default outputs
85+
output_id?: string | null;
8486
package?: PackagePolicyPackage;
8587
inputs: NewPackagePolicyInput[];
8688
vars?: PackagePolicyConfigRecord;

x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export type PostDeletePackagePoliciesResponse = Array<{
6363
package?: PackagePolicyPackage;
6464
policy_id?: string;
6565
policy_ids?: string[];
66+
output_id?: string;
6667
// Support generic errors
6768
statusCode?: number;
6869
body?: {

x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/components/hooks.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
import { useMemo } from 'react';
99
import { useHistory } from 'react-router-dom';
1010

11+
import { LICENCE_FOR_OUTPUT_PER_INTEGRATION } from '../../../../../../../../../common/constants';
12+
import { getAllowedOutputTypesForIntegration } from '../../../../../../../../../common/services/output_helpers';
13+
import { useGetOutputs, useLicense } from '../../../../../../hooks';
14+
1115
export function useDataStreamId() {
1216
const history = useHistory();
1317

@@ -16,3 +20,21 @@ export function useDataStreamId() {
1620
return searchParams.get('datastreamId') ?? undefined;
1721
}, [history.location.search]);
1822
}
23+
24+
export function useOutputs(packageName: string) {
25+
const licenseService = useLicense();
26+
const canUseOutputPerIntegration = licenseService.hasAtLeast(LICENCE_FOR_OUTPUT_PER_INTEGRATION);
27+
const { data: outputsData, isLoading } = useGetOutputs();
28+
const allowedOutputTypes = getAllowedOutputTypesForIntegration(packageName);
29+
const allowedOutputs = useMemo(() => {
30+
if (!outputsData || !canUseOutputPerIntegration) {
31+
return [];
32+
}
33+
return outputsData.items.filter((output) => allowedOutputTypes.includes(output.type));
34+
}, [allowedOutputTypes, canUseOutputPerIntegration, outputsData]);
35+
return {
36+
isLoading,
37+
canUseOutputPerIntegration,
38+
allowedOutputs,
39+
};
40+
}

x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/components/steps/step_define_package_policy.tsx

+55-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
EuiLink,
2121
EuiCallOut,
2222
EuiSpacer,
23+
EuiSelect,
2324
} from '@elastic/eui';
2425

2526
import styled from 'styled-components';
@@ -32,6 +33,7 @@ import { isAdvancedVar } from '../../services';
3233
import type { PackagePolicyValidationResults } from '../../services';
3334

3435
import { PackagePolicyInputVarField } from './components';
36+
import { useOutputs } from './components/hooks';
3537

3638
// on smaller screens, fields should be displayed in one column
3739
const FormGroupResponsiveFields = styled(EuiDescribedFormGroup)`
@@ -81,6 +83,14 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
8183
});
8284
}
8385

86+
// Outputs
87+
const {
88+
isLoading: isOutputsLoading,
89+
canUseOutputPerIntegration,
90+
allowedOutputs,
91+
} = useOutputs(packageInfo.name);
92+
93+
// Managed policy
8494
const isManaged = packagePolicy.is_managed;
8595

8696
return validationResults ? (
@@ -245,6 +255,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
245255
{isShowingAdvanced ? (
246256
<EuiFlexItem>
247257
<EuiFlexGroup direction="column" gutterSize="m">
258+
{/* Namespace */}
248259
<EuiFlexItem>
249260
<EuiFormRow
250261
isInvalid={!!validationResults.namespace}
@@ -264,7 +275,7 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
264275
) : (
265276
<FormattedMessage
266277
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyNamespaceHelpLabel"
267-
defaultMessage="Change the default namespace inherited from the selected Agent policy. This setting changes the name of the integration's data stream. {learnMore}."
278+
defaultMessage="Change the default namespace inherited from the parent agent policy. This setting changes the name of the integration's data stream. {learnMore}."
268279
values={{
269280
learnMore: (
270281
<EuiLink
@@ -304,6 +315,49 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
304315
/>
305316
</EuiFormRow>
306317
</EuiFlexItem>
318+
319+
{/* Output */}
320+
{canUseOutputPerIntegration && (
321+
<EuiFlexItem>
322+
<EuiFormRow
323+
label={
324+
<FormattedMessage
325+
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyOutputInputLabel"
326+
defaultMessage="Output"
327+
/>
328+
}
329+
helpText={
330+
<FormattedMessage
331+
id="xpack.fleet.createPackagePolicy.stepConfigure.packagePolicyOutputHelpLabel"
332+
defaultMessage="Change the default output inherited from the parent agent policy. This setting changes where the integration's data is sent."
333+
/>
334+
}
335+
>
336+
<EuiSelect
337+
data-test-subj="packagePolicyOutputInput"
338+
isLoading={isOutputsLoading}
339+
options={[
340+
{
341+
value: '',
342+
text: '',
343+
},
344+
...allowedOutputs.map((output) => ({
345+
value: output.id,
346+
text: output.name,
347+
})),
348+
]}
349+
value={packagePolicy.output_id || ''}
350+
onChange={(e) => {
351+
updatePackagePolicy({
352+
output_id: e.target.value.trim() || null,
353+
});
354+
}}
355+
/>
356+
</EuiFormRow>
357+
</EuiFlexItem>
358+
)}
359+
360+
{/* Data retention settings info */}
307361
<EuiFlexItem>
308362
<EuiFormRow
309363
label={

x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/package_policies/package_policies_table.tsx

+74-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import {
3737
usePermissionCheck,
3838
useStartServices,
3939
useMultipleAgentPolicies,
40+
useGetOutputs,
41+
useDefaultOutput,
4042
} from '../../../../../hooks';
4143
import { pkgKeyFromPackageInfo } from '../../../../../services';
4244

@@ -106,6 +108,16 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
106108
return packagePolicy.policy_ids.length || 0;
107109
}, []);
108110

111+
const { data: outputsData, isLoading: isOutputsLoading } = useGetOutputs();
112+
const { output: defaultOutputData } = useDefaultOutput();
113+
const outputNamesById = useMemo(() => {
114+
const outputs = outputsData?.items ?? [];
115+
return outputs.reduce<Record<string, string>>((acc, output) => {
116+
acc[output.id] = output.name;
117+
return acc;
118+
}, {});
119+
}, [outputsData]);
120+
109121
const columns = useMemo(
110122
(): EuiInMemoryTableProps<InMemoryPackagePolicy>['columns'] => [
111123
{
@@ -115,6 +127,7 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
115127
name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.nameColumnTitle', {
116128
defaultMessage: 'Integration policy',
117129
}),
130+
width: '35%',
118131
render: (value: string, packagePolicy: InMemoryPackagePolicy) => (
119132
<EuiFlexGroup gutterSize="s" alignItems="center">
120133
<EuiFlexItem data-test-subj="PackagePoliciesTableName" grow={false}>
@@ -278,10 +291,67 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
278291
);
279292
},
280293
},
294+
{
295+
field: 'output_id',
296+
name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.outputColumnTitle', {
297+
defaultMessage: 'Output',
298+
}),
299+
render: (outputId: InMemoryPackagePolicy['output_id']) => {
300+
if (isOutputsLoading) {
301+
return null;
302+
}
303+
if (outputId) {
304+
return <EuiBadge color="hollow">{outputNamesById[outputId] || outputId}</EuiBadge>;
305+
}
306+
if (agentPolicy.data_output_id) {
307+
return (
308+
<>
309+
<EuiBadge color="default">
310+
{outputNamesById[agentPolicy.data_output_id] || agentPolicy.data_output_id}
311+
</EuiBadge>
312+
&nbsp;
313+
<EuiIconTip
314+
content={i18n.translate(
315+
'xpack.fleet.policyDetails.packagePoliciesTable.outputFromParentPolicyText',
316+
{
317+
defaultMessage: 'Output defined in parent agent policy',
318+
}
319+
)}
320+
position="right"
321+
type="iInCircle"
322+
color="subdued"
323+
/>
324+
</>
325+
);
326+
}
327+
if (defaultOutputData) {
328+
return (
329+
<>
330+
<EuiBadge color="default">
331+
{outputNamesById[defaultOutputData.id] || defaultOutputData.id}
332+
</EuiBadge>
333+
&nbsp;
334+
<EuiIconTip
335+
content={i18n.translate(
336+
'xpack.fleet.policyDetails.packagePoliciesTable.outputFromFleetSettingsText',
337+
{
338+
defaultMessage: 'Output defined in Fleet settings',
339+
}
340+
)}
341+
position="right"
342+
type="iInCircle"
343+
color="subdued"
344+
/>
345+
</>
346+
);
347+
}
348+
},
349+
},
281350
{
282351
name: i18n.translate('xpack.fleet.policyDetails.packagePoliciesTable.actionsColumnTitle', {
283352
defaultMessage: 'Actions',
284353
}),
354+
width: '70px',
285355
actions: [
286356
{
287357
render: (packagePolicy: InMemoryPackagePolicy) => {
@@ -309,8 +379,11 @@ export const PackagePoliciesTable: React.FunctionComponent<Props> = ({
309379
agentPolicy,
310380
canUseMultipleAgentPolicies,
311381
canReadAgentPolicies,
312-
canWriteIntegrationPolicies,
313382
getSharedPoliciesNumber,
383+
canWriteIntegrationPolicies,
384+
isOutputsLoading,
385+
defaultOutputData,
386+
outputNamesById,
314387
]
315388
);
316389

0 commit comments

Comments
 (0)