[Fleet] Settings Framework API and UI (#179795)

## Summary

Follow up on https://github.com/elastic/kibana/pull/170539
Related to https://github.com/elastic/ingest-dev/issues/2471 (Phase 1)

Dynamically creating settings fields from configuration.
These settings are saved in the agent policy SO's `advanced_settings`
field.
In the current pr the agent policy read/create/update works including
the UI.
It still has to be extended to support a few more type of settings: e.g.
dropdown values, settings consisting of multiple fields.

<img width="2212" alt="image"
src="c2ee7187-41bf-42a4-8a22-f43ea8ccfb8c">

These settings are added to the full agent policy agents section:
<img width="687" alt="image"
src="05e244f3-148c-4c88-9b9c-fd665fd0154c">


## Old description:

Add support for saved object mapping and api field name,

## Example API calls and full policy generation

<img width="600" alt="Screenshot 2024-04-02 at 3 54 35 PM"
src="ee2ea087-3e02-4351-9138-c56ace1d5b34">
<img width="500" alt="Screenshot 2024-04-02 at 4 13 42 PM"
src="97514b2e-ef39-475e-8a88-1204ce2c0cda">
<img width="600" alt="Screenshot 2024-04-02 at 4 13 54 PM"
src="6553e133-ea07-48b5-b0ec-c45861b9b246">
<img width="600" alt="Screenshot 2024-04-02 at 5 42 27 PM"
src="faa560fd-7303-46f5-ae2e-034fc10dddfd">


## Open questions/Issues

### Saved objects

*I think we will still have to do some work to add a new model version
when adding a new saved object field, I do not see an easy way to
programatically generate that. In a first time it probably could be a
manual action to add those migration

### API

Open api generation, I think as a first iteration it could be a manual
operation to update openAPI spec, but we should be able to
programatically generate that with a script in the future

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com>
Co-authored-by: Julia Bardi <julia.bardi@elastic.co>
This commit is contained in:
Nicolas Chaulet 2024-04-09 20:18:40 +07:00 committed by GitHub
parent f7ebd29b33
commit c88b3bdc95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 785 additions and 52 deletions

View file

@ -180,7 +180,7 @@ export const HASH_TO_VERSION_MAP = {
'infrastructure-monitoring-log-view|c50526fc6040c5355ed027d34d05b35c': '10.0.0',
'infrastructure-ui-source|3d1b76c39bfb2cc8296b024d73854724': '10.0.0',
'ingest_manager_settings|b91ffb075799c78ffd7dbd51a279c8c9': '10.1.0',
'ingest-agent-policies|20768dc7ce5eced3eb309e50d8a6cf76': '10.0.0',
'ingest-agent-policies|0fd93cd11c019b118e93a9157c22057b': '10.1.0',
'ingest-download-sources|0b0f6828e59805bd07a650d80817c342': '10.0.0',
'ingest-outputs|b1237f7fdc0967709e75d65d208ace05': '10.6.0',
'ingest-package-policies|a1a074bad36e68d54f98d2158d60f879': '10.0.0',

View file

@ -471,6 +471,7 @@
],
"infrastructure-ui-source": [],
"ingest-agent-policies": [
"advanced_settings",
"agent_features",
"agent_features.enabled",
"agent_features.name",

View file

@ -1590,6 +1590,10 @@
},
"ingest-agent-policies": {
"properties": {
"advanced_settings": {
"index": false,
"type": "flattened"
},
"agent_features": {
"properties": {
"enabled": {

View file

@ -106,7 +106,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"infra-custom-dashboards": "1a5994f2e05bb8a1609825ddbf5012f77c5c67f3",
"infrastructure-monitoring-log-view": "5f86709d3c27aed7a8379153b08ee5d3d90d77f5",
"infrastructure-ui-source": "113182d6895764378dfe7fa9fa027244f3a457c4",
"ingest-agent-policies": "7633e578f60c074f8267bc50ec4763845e431437",
"ingest-agent-policies": "d2ee0bf36a512c2ac744b0def1c822b7880f1f83",
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
"ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91",
"ingest-package-policies": "8a99e165aab00c6c365540427a3abeb7bea03f31",

View file

@ -29,6 +29,7 @@ export const allowedExperimentalValues = Object.freeze<Record<string, boolean>>(
enableStrictKQLValidation: false,
subfeaturePrivileges: false,
enablePackagesStateMachine: true,
advancedPolicySettings: true,
});
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;

View file

@ -7383,6 +7383,11 @@
"type": "object",
"description": "Override settings that are defined in the agent policy. Input settings cannot be overridden. The override option should be used only in unusual circumstances and not as a routine procedure.",
"nullable": true
},
"advanced_settings": {
"type": "object",
"description": "Advanced settings stored in the agent policy, e.g. agent_limits_go_max_procs",
"nullable": true
}
},
"required": [

View file

@ -4734,6 +4734,12 @@ components:
settings cannot be overridden. The override option should be used
only in unusual circumstances and not as a routine procedure.
nullable: true
advanced_settings:
type: object
description: >-
Advanced settings stored in the agent policy, e.g.
agent_limits_go_max_procs
nullable: true
required:
- id
- status

View file

@ -69,6 +69,10 @@ properties:
type: object
description: Override settings that are defined in the agent policy. Input settings cannot be overridden. The override option should be used only in unusual circumstances and not as a routine procedure.
nullable: true
advanced_settings:
type: object
description: Advanced settings stored in the agent policy, e.g. agent_limits_go_max_procs
nullable: true
required:
- id
- status

View file

@ -0,0 +1,100 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { z } from 'zod';
import type { SettingsConfig } from './types';
export const zodStringWithDurationValidation = z
.string()
.refine((val) => val.match(/^(\d+[s|m|h|d])?$/), {
message: i18n.translate(
'xpack.fleet.settings.agentPolicyAdvanced.downloadTimeoutValidationMessage',
{
defaultMessage: 'Must be a string with a time unit, e.g. 30s, 5m, 2h, 1d',
}
),
});
export const AGENT_POLICY_ADVANCED_SETTINGS: SettingsConfig[] = [
{
name: 'agent.limits.go_max_procs',
title: i18n.translate('xpack.fleet.settings.agentPolicyAdvanced.goMaxProcsTitle', {
defaultMessage: 'GO_MAX_PROCS',
}),
description: i18n.translate('xpack.fleet.settings.agentPolicyAdvanced.goMaxProcsDescription', {
defaultMessage: 'Limits the maximum number of CPUs that can be executing simultaneously',
}),
learnMoreLink:
'https://www.elastic.co/guide/en/fleet/current/enable-custom-policy-settings.html#limit-cpu-usage',
api_field: {
name: 'agent_limits_go_max_procs',
},
schema: z.number().int().min(0).default(0),
},
{
name: 'agent.download.timeout',
title: i18n.translate('xpack.fleet.settings.agentPolicyAdvanced.downloadTimeoutTitle', {
defaultMessage: 'Agent binary download timeout',
}),
description: i18n.translate(
'xpack.fleet.settings.agentPolicyAdvanced.downloadTimeoutDescription',
{
defaultMessage: 'Timeout in seconds for downloading the agent binary',
}
),
learnMoreLink:
'https://www.elastic.co/guide/en/fleet/current/enable-custom-policy-settings.html#configure-agent-download-timeout',
api_field: {
name: 'agent_download_timeout',
},
schema: zodStringWithDurationValidation.default('120s'),
},
{
name: 'agent.download.target_directory',
api_field: {
name: 'agent_download_target_directory',
},
title: i18n.translate(
'xpack.fleet.settings.agentPolicyAdvanced.agentDownloadTargetDirectoryTitle',
{
defaultMessage: 'Agent binary target directory',
}
),
description: i18n.translate(
'xpack.fleet.settings.agentPolicyAdvanced.agentDownloadTargetDirectoryDescription',
{
defaultMessage: 'The disk path to which the agent binary will be downloaded',
}
),
learnMoreLink:
'https://www.elastic.co/guide/en/fleet/current/elastic-agent-standalone-download.html',
schema: z.string(),
},
{
name: 'agent.logging.metrics.period',
api_field: {
name: 'agent_logging_metrics_period',
},
title: i18n.translate(
'xpack.fleet.settings.agentPolicyAdvanced.agentLoggingMetricsPeriodTitle',
{
defaultMessage: 'Agent logging metrics period',
}
),
description: i18n.translate(
'xpack.fleet.settings.agentPolicyAdvanced.agentLoggingMetricsPeriodDescription',
{
defaultMessage: 'The frequency of agent metrics logging',
}
),
learnMoreLink:
'https://www.elastic.co/guide/en/fleet/current/elastic-agent-standalone-logging-config.html#elastic-agent-standalone-logging-settings',
schema: zodStringWithDurationValidation.default('30s'),
},
];

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export { AGENT_POLICY_ADVANCED_SETTINGS } from './agent_policy_settings';

View file

@ -0,0 +1,21 @@
/*
* 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 { z } from 'zod';
export type SettingsSection = 'AGENT_POLICY_ADVANCED_SETTINGS';
export interface SettingsConfig {
name: string;
title: string;
description: string;
learnMoreLink?: string;
schema: z.ZodTypeAny;
api_field: {
name: string;
};
}

View file

@ -39,6 +39,7 @@ export interface NewAgentPolicy {
agent_features?: Array<{ name: string; enabled: boolean }>;
is_protected?: boolean;
overrides?: { [key: string]: any } | null;
advanced_settings?: { [key: string]: any } | null;
}
// SO definition for this type is declared in server/types/interfaces

View file

@ -0,0 +1,104 @@
/*
* 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 { act, fireEvent } from '@testing-library/react';
import React from 'react';
import { z } from 'zod';
import { zodStringWithDurationValidation } from '../../../../../common/settings/agent_policy_settings';
import type { SettingsConfig } from '../../../../../common/settings/types';
import { createFleetTestRendererMock } from '../../../../mock';
import { ConfiguredSettings } from '.';
const mockUpdateAgentPolicy = jest.fn();
const mockUpdateAdvancedSettingsHasErrors = jest.fn();
jest.mock('../../sections/agent_policy/components/agent_policy_form', () => ({
useAgentPolicyFormContext: () => ({
updateAdvancedSettingsHasErrors: mockUpdateAdvancedSettingsHasErrors,
updateAgentPolicy: mockUpdateAgentPolicy,
agentPolicy: {
advanced_settings: {
agent_limits_go_max_procs: 0,
agent_download_timeout: '120s',
},
},
}),
}));
describe('ConfiguredSettings', () => {
const testRenderer = createFleetTestRendererMock();
beforeEach(() => {
jest.clearAllMocks();
});
function render(settingsConfig: SettingsConfig[]) {
return testRenderer.render(<ConfiguredSettings configuredSettings={settingsConfig} />);
}
it('should render number field', () => {
const result = render([
{
name: 'agent.limits.go_max_procs',
title: 'GO_MAX_PROCS',
description: 'Description',
learnMoreLink: '',
api_field: {
name: 'agent_limits_go_max_procs',
},
schema: z.number().int().min(0).default(0),
},
]);
expect(result.getByText('GO_MAX_PROCS')).not.toBeNull();
const input = result.getByTestId('configuredSetting-agent.limits.go_max_procs');
expect(input).toHaveValue(0);
act(() => {
fireEvent.change(input, { target: { value: '1' } });
});
expect(mockUpdateAgentPolicy).toHaveBeenCalledWith(
expect.objectContaining({
advanced_settings: expect.objectContaining({ agent_limits_go_max_procs: 1 }),
})
);
});
it('should render string field with time duration validation', () => {
const result = render([
{
name: 'agent.download.timeout',
title: 'Agent binary download timeout',
description: 'Description',
learnMoreLink: '',
api_field: {
name: 'agent_download_timeout',
},
schema: zodStringWithDurationValidation.default('120s'),
},
]);
expect(result.getByText('Agent binary download timeout')).not.toBeNull();
const input = result.getByTestId('configuredSetting-agent.download.timeout');
expect(input).toHaveValue('120s');
act(() => {
fireEvent.change(input, { target: { value: '120' } });
});
expect(input).toHaveAttribute('aria-invalid', 'true');
expect(
result.getByText('Must be a string with a time unit, e.g. 30s, 5m, 2h, 1d')
).not.toBeNull();
expect(mockUpdateAdvancedSettingsHasErrors).toHaveBeenCalledWith(true);
});
});

View file

@ -0,0 +1,157 @@
/*
* 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 { z, ZodFirstPartyTypeKind } from 'zod';
import React, { useState } from 'react';
import {
EuiDescribedFormGroup,
EuiFieldNumber,
EuiFieldText,
EuiFormRow,
EuiLink,
} from '@elastic/eui';
import type { SettingsConfig } from '../../../../../common/settings/types';
import { useAgentPolicyFormContext } from '../../sections/agent_policy/components/agent_policy_form';
export const settingComponentRegistry = new Map<
string,
(settingsconfig: SettingsConfig) => React.ReactElement
>();
settingComponentRegistry.set(ZodFirstPartyTypeKind.ZodNumber, (settingsConfig) => {
return (
<SettingsFieldWrapper
settingsConfig={settingsConfig}
typeName={ZodFirstPartyTypeKind.ZodNumber}
renderItem={({ fieldKey, fieldValue, handleChange, isInvalid, coercedSchema }: any) => (
<EuiFieldNumber
fullWidth
data-test-subj={fieldKey}
value={fieldValue}
onChange={handleChange}
isInvalid={isInvalid}
min={coercedSchema.minValue ?? undefined}
max={coercedSchema.maxValue ?? undefined}
/>
)}
/>
);
});
settingComponentRegistry.set(ZodFirstPartyTypeKind.ZodString, (settingsConfig) => {
return (
<SettingsFieldWrapper
settingsConfig={settingsConfig}
typeName={ZodFirstPartyTypeKind.ZodString}
renderItem={({ fieldKey, fieldValue, handleChange, isInvalid }: any) => (
<EuiFieldText
fullWidth
data-test-subj={fieldKey}
value={fieldValue}
onChange={handleChange}
isInvalid={isInvalid}
/>
)}
/>
);
});
const SettingsFieldWrapper: React.FC<{
settingsConfig: SettingsConfig;
typeName: keyof typeof ZodFirstPartyTypeKind;
renderItem: Function;
}> = ({ settingsConfig, typeName, renderItem }) => {
const [error, setError] = useState('');
const agentPolicyFormContext = useAgentPolicyFormContext();
const fieldKey = `configuredSetting-${settingsConfig.name}`;
const defaultValue: number =
settingsConfig.schema instanceof z.ZodDefault
? settingsConfig.schema._def.defaultValue()
: undefined;
const coercedSchema = settingsConfig.schema as z.ZodString;
const convertValue = (value: string, type: keyof typeof ZodFirstPartyTypeKind): any => {
if (type === ZodFirstPartyTypeKind.ZodNumber) {
if (value === '') {
return 0;
}
return parseInt(value, 10);
}
return value;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = convertValue(e.target.value, typeName);
const validationResults = coercedSchema.safeParse(newValue);
if (!validationResults.success) {
setError(validationResults.error.issues[0].message);
agentPolicyFormContext?.updateAdvancedSettingsHasErrors(true);
} else {
setError('');
agentPolicyFormContext?.updateAdvancedSettingsHasErrors(false);
}
const newAdvancedSettings = {
...(agentPolicyFormContext?.agentPolicy.advanced_settings ?? {}),
[settingsConfig.api_field.name]: newValue,
};
agentPolicyFormContext?.updateAgentPolicy({ advanced_settings: newAdvancedSettings });
};
const fieldValue =
agentPolicyFormContext?.agentPolicy.advanced_settings?.[settingsConfig.api_field.name] ??
defaultValue;
return (
<EuiDescribedFormGroup
fullWidth
title={<h4>{settingsConfig.title}</h4>}
description={
<>
{settingsConfig.description}.{' '}
<EuiLink href={settingsConfig.learnMoreLink} external>
Learn more.
</EuiLink>
</>
}
>
<EuiFormRow fullWidth key={fieldKey} error={error} isInvalid={!!error}>
{renderItem({ fieldValue, handleChange, isInvalid: !!error, fieldKey, coercedSchema })}
</EuiFormRow>
</EuiDescribedFormGroup>
);
};
export function ConfiguredSettings({
configuredSettings,
}: {
configuredSettings: SettingsConfig[];
}) {
return (
<>
{configuredSettings.map((configuredSetting) => {
const Component = settingComponentRegistry.get(
configuredSetting.schema instanceof z.ZodDefault
? configuredSetting.schema._def.innerType._def.typeName === 'ZodEffects'
? configuredSetting.schema._def.innerType._def.schema._def.typeName
: configuredSetting.schema._def.innerType._def.typeName
: configuredSetting.schema._def.typeName
);
if (!Component) {
throw new Error(`Unknown setting type: ${configuredSetting.schema._type}}`);
}
return <Component key={configuredSetting.name} {...configuredSetting} />;
})}
</>
);
}

View file

@ -12,13 +12,19 @@ import {
EuiForm,
EuiHorizontalRule,
EuiSpacer,
EuiTitle,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import styled from 'styled-components';
import { AGENT_POLICY_ADVANCED_SETTINGS } from '../../../../../../common/settings';
import type { NewAgentPolicy, AgentPolicy } from '../../../types';
import { useAuthz } from '../../../../../hooks';
import { ConfiguredSettings } from '../../../components/form_settings';
import { ExperimentalFeaturesService } from '../../../../../services';
import { AgentPolicyAdvancedOptionsContent } from './agent_policy_advanced_fields';
import { AgentPolicyGeneralFields } from './agent_policy_general_fields';
import { AgentPolicyFormSystemMonitoringCheckbox } from './agent_policy_system_monitoring_field';
@ -37,7 +43,21 @@ interface Props {
updateSysMonitoring: (newValue: boolean) => void;
validation: ValidationResults;
isEditing?: boolean;
// form error state is passed up to the form
updateAdvancedSettingsHasErrors: (hasErrors: boolean) => void;
}
const AgentPolicyFormContext = React.createContext<
| {
agentPolicy: Partial<NewAgentPolicy | AgentPolicy> & { [key: string]: any };
updateAgentPolicy: (u: Partial<NewAgentPolicy | AgentPolicy>) => void;
updateAdvancedSettingsHasErrors: (hasErrors: boolean) => void;
}
| undefined
>(undefined);
export const useAgentPolicyFormContext = () => {
return React.useContext(AgentPolicyFormContext);
};
export const AgentPolicyForm: React.FunctionComponent<Props> = ({
agentPolicy,
@ -46,10 +66,13 @@ export const AgentPolicyForm: React.FunctionComponent<Props> = ({
updateSysMonitoring,
validation,
isEditing = false,
updateAdvancedSettingsHasErrors,
}) => {
const authz = useAuthz();
const disabled = !authz.fleet.allAgents;
const { advancedPolicySettings } = ExperimentalFeaturesService.get();
const generalSettingsWrapper = (children: JSX.Element[]) => (
<EuiDescribedFormGroup
title={
@ -72,62 +95,102 @@ export const AgentPolicyForm: React.FunctionComponent<Props> = ({
);
return (
<EuiForm>
{!isEditing ? (
<AgentPolicyGeneralFields
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
validation={validation}
disabled={disabled}
/>
) : (
generalSettingsWrapper([
<AgentPolicyFormContext.Provider
value={{ agentPolicy, updateAgentPolicy, updateAdvancedSettingsHasErrors }}
>
<EuiForm>
{!isEditing ? (
<AgentPolicyGeneralFields
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
validation={validation}
disabled={disabled}
/>,
])
)}
{!isEditing ? (
<AgentPolicyFormSystemMonitoringCheckbox
withSysMonitoring={withSysMonitoring}
updateSysMonitoring={updateSysMonitoring}
/>
) : null}
{!isEditing ? (
<>
<EuiHorizontalRule />
<EuiSpacer size="xs" />
<StyledEuiAccordion
id="advancedOptions"
buttonContent={
<FormattedMessage
id="xpack.fleet.agentPolicyForm.advancedOptionsToggleLabel"
defaultMessage="Advanced options"
/>
) : (
generalSettingsWrapper([
<AgentPolicyGeneralFields
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
validation={validation}
disabled={disabled}
/>,
])
)}
{!isEditing ? (
<AgentPolicyFormSystemMonitoringCheckbox
withSysMonitoring={withSysMonitoring}
updateSysMonitoring={updateSysMonitoring}
/>
) : null}
{!isEditing ? (
<>
<EuiHorizontalRule />
<EuiSpacer size="xs" />
<StyledEuiAccordion
id="advancedOptions"
buttonContent={
<FormattedMessage
id="xpack.fleet.agentPolicyForm.advancedOptionsToggleLabel"
defaultMessage="Advanced options"
/>
}
buttonClassName="ingest-active-button"
>
<EuiSpacer size="l" />
<AgentPolicyAdvancedOptionsContent
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
validation={validation}
isEditing={isEditing}
/>
}
buttonClassName="ingest-active-button"
>
<EuiSpacer size="l" />
{advancedPolicySettings ? (
<>
<EuiSpacer size="xl" />
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.advancedSettingsTitle"
defaultMessage="Advanced settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<ConfiguredSettings configuredSettings={AGENT_POLICY_ADVANCED_SETTINGS} />
</>
) : null}
</StyledEuiAccordion>
</>
) : (
<>
<AgentPolicyAdvancedOptionsContent
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
validation={validation}
isEditing={isEditing}
disabled={disabled}
/>
</StyledEuiAccordion>
</>
) : (
<AgentPolicyAdvancedOptionsContent
agentPolicy={agentPolicy}
updateAgentPolicy={updateAgentPolicy}
validation={validation}
isEditing={isEditing}
disabled={disabled}
/>
)}
</EuiForm>
{advancedPolicySettings ? (
<>
<EuiSpacer size="xl" />
<EuiTitle>
<h3>
<FormattedMessage
id="xpack.fleet.agentPolicyForm.advancedSettingsTitle"
defaultMessage="Advanced settings"
/>
</h3>
</EuiTitle>
<EuiSpacer size="m" />
<ConfiguredSettings configuredSettings={AGENT_POLICY_ADVANCED_SETTINGS} />
</>
) : null}
<EuiSpacer size="xl" />
</>
)}
</EuiForm>
</AgentPolicyFormContext.Provider>
);
};

View file

@ -52,6 +52,7 @@ const pickAgentPolicyKeysToSend = (agentPolicy: AgentPolicy) =>
'fleet_server_host_id',
'agent_features',
'is_protected',
'advanced_settings',
]);
const FormWrapper = styled.div`
@ -77,6 +78,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
const [agentCount, setAgentCount] = useState<number>(0);
const [withSysMonitoring, setWithSysMonitoring] = useState<boolean>(true);
const validation = agentPolicyFormValidation(agentPolicy);
const [hasAdvancedSettingsErrors, setHasAdvancedSettingsErrors] = useState<boolean>(false);
const updateAgentPolicy = (updatedFields: Partial<AgentPolicy>) => {
setAgentPolicy({
@ -169,6 +171,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
updateSysMonitoring={(newValue) => setWithSysMonitoring(newValue)}
validation={validation}
isEditing={true}
updateAdvancedSettingsHasErrors={setHasAdvancedSettingsErrors}
/>
{hasChanges ? (
@ -202,7 +205,11 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
{showDevtoolsRequest ? (
<EuiFlexItem grow={false}>
<DevtoolsRequestFlyoutButton
isDisabled={isLoading || Object.keys(validation).length > 0}
isDisabled={
isLoading ||
Object.keys(validation).length > 0 ||
hasAdvancedSettingsErrors
}
btnProps={{
color: 'text',
}}
@ -221,7 +228,10 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
onClick={onSubmit}
isLoading={isLoading}
isDisabled={
!hasFleetAllPrivileges || isLoading || Object.keys(validation).length > 0
!hasFleetAllPrivileges ||
isLoading ||
Object.keys(validation).length > 0 ||
hasAdvancedSettingsErrors
}
iconType="save"
color="primary"

View file

@ -53,6 +53,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({
const [isLoading, setIsLoading] = useState<boolean>(false);
const [withSysMonitoring, setWithSysMonitoring] = useState<boolean>(true);
const validation = agentPolicyFormValidation(agentPolicy);
const [hasAdvancedSettingsErrors, setHasAdvancedSettingsErrors] = useState<boolean>(false);
const updateAgentPolicy = (updatedFields: Partial<NewAgentPolicy>) => {
setAgentPolicy({
@ -95,6 +96,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({
withSysMonitoring={withSysMonitoring}
updateSysMonitoring={(newValue) => setWithSysMonitoring(newValue)}
validation={validation}
updateAdvancedSettingsHasErrors={setHasAdvancedSettingsErrors}
/>
</EuiFlyoutBody>
);
@ -120,7 +122,9 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({
{showDevtoolsRequest ? (
<EuiFlexItem grow={false}>
<DevtoolsRequestFlyoutButton
isDisabled={isLoading || Object.keys(validation).length > 0}
isDisabled={
isLoading || Object.keys(validation).length > 0 || hasAdvancedSettingsErrors
}
description={i18n.translate(
'xpack.fleet.createAgentPolicy.devtoolsRequestDescription',
{
@ -136,7 +140,10 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent<Props> = ({
fill
isLoading={isLoading}
isDisabled={
!hasFleetAllPrivileges || isLoading || Object.keys(validation).length > 0
!hasFleetAllPrivileges ||
isLoading ||
Object.keys(validation).length > 0 ||
hasAdvancedSettingsErrors
}
onClick={async () => {
setIsLoading(true);

View file

@ -156,6 +156,7 @@ export const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
is_protected: { type: 'boolean' },
overrides: { type: 'flattened', index: false },
keep_monitoring_alive: { type: 'boolean' },
advanced_settings: { type: 'flattened', index: false },
},
},
migrations: {
@ -165,6 +166,18 @@ export const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
'8.5.0': migrateAgentPolicyToV850,
'8.9.0': migrateAgentPolicyToV890,
},
modelVersions: {
'1': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
advanced_settings: { type: 'flattened', index: false },
},
},
],
},
},
},
[OUTPUT_SAVED_OBJECT_TYPE]: {
name: OUTPUT_SAVED_OBJECT_TYPE,

View file

@ -714,6 +714,30 @@ describe('getFullAgentPolicy', () => {
revision: 1,
});
});
it('should return a policy with advanced settings', async () => {
mockAgentPolicy({
advanced_settings: {
agent_limits_go_max_procs: 2,
agent_download_timeout: '60s',
agent_download_target_directory: '/tmp',
agent_logging_metrics_period: '10s',
},
});
const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy');
expect(agentPolicy).toMatchObject({
id: 'agent-policy',
agent: {
download: {
timeout: '60s',
target_directory: '/tmp',
},
limits: { go_max_procs: 2 },
logging: { metrics: { period: '10s' } },
},
});
});
});
describe('transformOutputToFullPolicyOutput', () => {

View file

@ -10,6 +10,7 @@
import type { SavedObjectsClientContract } from '@kbn/core/server';
import { safeLoad } from 'js-yaml';
import deepMerge from 'deepmerge';
import { set } from '@kbn/safer-lodash-set';
import {
getDefaultPresetForEsOutput,
@ -37,7 +38,7 @@ import {
kafkaCompressionType,
outputType,
} from '../../../common/constants';
import { getSettingsValuesForAgentPolicy } from '../form_settings';
import { getPackageInfo } from '../epm/packages';
import { pkgToPkgKey, splitPkgKey } from '../epm/registry';
import { appContextService } from '../app_context';
@ -242,6 +243,14 @@ export async function getFullAgentPolicy(
fullAgentPolicy.fleet = generateFleetConfig(fleetServerHosts, proxies);
}
const settingsValues = getSettingsValuesForAgentPolicy(
'AGENT_POLICY_ADVANCED_SETTINGS',
agentPolicy
);
Object.entries(settingsValues).forEach(([settingsKey, settingValue]) => {
set(fullAgentPolicy, settingsKey, settingValue);
});
// populate protection and signed properties
const messageSigningService = appContextService.getMessageSigningService();
if (messageSigningService && fullAgentPolicy.agent) {

View file

@ -0,0 +1,75 @@
/*
* 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 { z } from 'zod';
import { schema } from '@kbn/config-schema';
import type { SettingsConfig } from '../../../common/settings/types';
import { _getSettingsAPISchema, _getSettingsValuesForAgentPolicy } from './form_settings';
const TEST_SETTINGS: SettingsConfig[] = [
{
name: 'test.foo',
title: 'test',
description: 'test',
schema: z.boolean(),
api_field: {
name: 'test_foo',
},
},
{
name: 'test.foo.default_value',
title: 'test',
description: 'test',
schema: z.string().default('test'),
api_field: {
name: 'test_foo_default_value',
},
},
];
describe('form_settings', () => {
describe('_getSettingsAPISchema', () => {
it('generate a valid API schema for api_field', () => {
const apiSchema = schema.object(_getSettingsAPISchema(TEST_SETTINGS));
expect(() =>
apiSchema.validate({
advanced_settings: {
test_foo: 'not valid',
},
})
).toThrowError(/Expected boolean, received string/);
expect(() =>
apiSchema.validate({
advanced_settings: {
test_foo: true,
},
})
).not.toThrow();
});
it('generate a valid API schema for api_field with default value', () => {
const apiSchema = schema.object(_getSettingsAPISchema(TEST_SETTINGS));
const res = apiSchema.validate({ advanced_settings: {} });
expect(res).toEqual({ advanced_settings: { test_foo_default_value: 'test' } });
});
});
describe('_getSettingsValuesForAgentPolicy', () => {
it('generate the proper values for agent policy (full agent policy)', () => {
const res = _getSettingsValuesForAgentPolicy(TEST_SETTINGS, {
advanced_settings: {
test_foo_default_value: 'test',
},
} as any);
expect(res).toEqual({ 'test.foo.default_value': 'test' });
});
});
});

View file

@ -0,0 +1,105 @@
/*
* 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 Props, schema } from '@kbn/config-schema';
import { stringifyZodError } from '@kbn/zod-helpers';
import type { SettingsConfig, SettingsSection } from '../../../common/settings/types';
import { AGENT_POLICY_ADVANCED_SETTINGS } from '../../../common/settings';
import type { AgentPolicy } from '../../types';
export function getSettingsAPISchema(settingSection: SettingsSection) {
const settings = getSettings(settingSection);
return _getSettingsAPISchema(settings);
}
export function _getSettingsAPISchema(settings: SettingsConfig[]): Props {
const validations: Props = {};
settings.forEach((setting) => {
if (!setting.api_field) {
return;
}
const defaultValueRes = setting.schema.safeParse(undefined);
const defaultValue = defaultValueRes.success ? defaultValueRes.data : undefined;
if (defaultValue) {
validations[setting.api_field.name] = schema.oneOf(
[
schema.any({
validate: (val: any) => {
const res = setting.schema.safeParse(val);
if (!res.success) {
return stringifyZodError(res.error);
}
},
}),
schema.literal(null),
],
{
defaultValue,
}
);
} else {
validations[setting.api_field.name] = schema.maybe(
schema.nullable(
schema.any({
validate: (val: any) => {
const res = setting.schema.safeParse(val);
if (!res.success) {
return stringifyZodError(res.error);
}
},
})
)
);
}
});
const advancedSettingsValidations: Props = {
advanced_settings: schema.maybe(
schema.object({
...validations,
})
),
};
return advancedSettingsValidations;
}
export function getSettingsValuesForAgentPolicy(
settingSection: SettingsSection,
agentPolicy: AgentPolicy
) {
const settings = getSettings(settingSection);
return _getSettingsValuesForAgentPolicy(settings, agentPolicy);
}
export function _getSettingsValuesForAgentPolicy(
settings: SettingsConfig[],
agentPolicy: AgentPolicy
) {
const settingsValues: { [k: string]: any } = {};
settings.forEach((setting) => {
if (!setting.api_field) {
return;
}
const val = agentPolicy.advanced_settings?.[setting.api_field.name];
if (val) {
settingsValues[setting.name] = val;
}
});
return settingsValues;
}
export function getSettings(settingSection: SettingsSection) {
if (settingSection === 'AGENT_POLICY_ADVANCED_SETTINGS') {
return AGENT_POLICY_ADVANCED_SETTINGS;
}
throw new Error(`Invalid settings section ${settingSection}`);
}

View file

@ -0,0 +1,12 @@
/*
* 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.
*/
export {
getSettings,
getSettingsAPISchema,
getSettingsValuesForAgentPolicy,
} from './form_settings';

View file

@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema';
import { agentPolicyStatuses, dataTypes } from '../../../common/constants';
import { isValidNamespace } from '../../../common/services';
import { getSettingsAPISchema } from '../../services/form_settings';
import { PackagePolicySchema } from './package_policy';
@ -81,6 +82,7 @@ export const AgentPolicyBaseSchema = {
})
)
),
...getSettingsAPISchema('AGENT_POLICY_ADVANCED_SETTINGS'),
};
export const NewAgentPolicySchema = schema.object({

View file

@ -105,5 +105,6 @@
"@kbn/core-http-server-mocks",
"@kbn/code-editor",
"@kbn/core-test-helpers-model-versions",
"@kbn/zod-helpers",
]
}