[APM] update policy editor with additional config values (#109516) (#109975)

* refactoring apm integration sections

* adding agent auth section

* refactoring

* adding some unit tests

* fixing ts issue

* removing unnecessary section

* hide fields

* removing suggestions when combo

* fixing apm migration

* addressing PR comments

* addressing PR changes

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-08-25 08:52:15 -04:00 committed by GitHub
parent f274289504
commit 18b31701e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 910 additions and 594 deletions

View file

@ -21,7 +21,7 @@ export function CreateAPMPolicyForm({ newPolicy, onChange }: Props) {
const [firstInput, ...restInputs] = newPolicy?.inputs;
const vars = firstInput?.vars;
function handleChange(newVars: PackagePolicyVars, isValid: boolean) {
function updateAPMPolicy(newVars: PackagePolicyVars, isValid: boolean) {
onChange({
isValid,
updatedPolicy: {
@ -31,6 +31,10 @@ export function CreateAPMPolicyForm({ newPolicy, onChange }: Props) {
});
}
return (
<APMPolicyForm vars={vars} onChange={handleChange} isCloudPolicy={false} />
<APMPolicyForm
vars={vars}
updateAPMPolicy={updateAPMPolicy}
isCloudPolicy={false}
/>
);
}

View file

@ -24,7 +24,7 @@ export function EditAPMPolicyForm({ newPolicy, onChange }: Props) {
const [firstInput, ...restInputs] = newPolicy?.inputs;
const vars = firstInput?.vars;
function handleChange(newVars: PackagePolicyVars, isValid: boolean) {
function updateAPMPolicy(newVars: PackagePolicyVars, isValid: boolean) {
onChange({
isValid,
updatedPolicy: {
@ -35,7 +35,7 @@ export function EditAPMPolicyForm({ newPolicy, onChange }: Props) {
return (
<APMPolicyForm
vars={vars}
onChange={handleChange}
updateAPMPolicy={updateAPMPolicy}
isCloudPolicy={newPolicy.policy_id === POLICY_ELASTIC_AGENT_ON_CLOUD}
/>
);

View file

@ -5,30 +5,124 @@
* 2.0.
*/
import { EuiSpacer } from '@elastic/eui';
import React from 'react';
import { OnFormChangeFn, PackagePolicyVars } from './typings';
import { APMSettingsForm } from './settings/apm_settings';
import { RUMSettingsForm } from './settings/rum_settings';
import { TLSSettingsForm } from './settings/tls_settings';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { getAgentAuthorizationSettings } from './settings_definition/agent_authorization_settings';
import { getApmSettings } from './settings_definition/apm_settings';
import {
getRUMSettings,
isRUMFormValid,
} from './settings_definition/rum_settings';
import {
getTLSSettings,
isTLSFormValid,
} from './settings_definition/tls_settings';
import { SettingsForm, SettingsSection } from './settings_form';
import { isSettingsFormValid, mergeNewVars } from './settings_form/utils';
import { PackagePolicyVars } from './typings';
interface Props {
onChange: OnFormChangeFn;
updateAPMPolicy: (newVars: PackagePolicyVars, isValid: boolean) => void;
vars?: PackagePolicyVars;
isCloudPolicy: boolean;
}
export function APMPolicyForm({ vars = {}, isCloudPolicy, onChange }: Props) {
export function APMPolicyForm({
vars = {},
isCloudPolicy,
updateAPMPolicy,
}: Props) {
const {
apmSettings,
rumSettings,
tlsSettings,
agentAuthorizationSettings,
} = useMemo(() => {
return {
apmSettings: getApmSettings({ isCloudPolicy }),
rumSettings: getRUMSettings(),
tlsSettings: getTLSSettings(),
agentAuthorizationSettings: getAgentAuthorizationSettings({
isCloudPolicy,
}),
};
}, [isCloudPolicy]);
function handleFormChange(key: string, value: any) {
// Merge new key/value with the rest of fields
const newVars = mergeNewVars(vars, key, value);
// Validate the entire form before sending it to fleet
const isFormValid =
isSettingsFormValid(apmSettings, newVars) &&
isRUMFormValid(newVars, rumSettings) &&
isTLSFormValid(newVars, tlsSettings) &&
isSettingsFormValid(agentAuthorizationSettings, newVars);
updateAPMPolicy(newVars, isFormValid);
}
const settingsSections: SettingsSection[] = [
{
id: 'apm',
title: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.settings.title',
{ defaultMessage: 'General' }
),
subtitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.settings.subtitle',
{ defaultMessage: 'Settings for the APM integration.' }
),
settings: apmSettings,
},
{
id: 'rum',
title: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.settings.title',
{ defaultMessage: 'Real User Monitoring' }
),
subtitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.settings.subtitle',
{ defaultMessage: 'Manage the configuration of the RUM JS agent.' }
),
settings: rumSettings,
},
{
id: 'tls',
title: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.settings.title',
{ defaultMessage: 'TLS Settings' }
),
subtitle: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.settings.subtitle',
{ defaultMessage: 'Settings for TLS certification.' }
),
settings: tlsSettings,
},
{
id: 'agentAuthorization',
title: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.settings.title',
{ defaultMessage: 'Agent authorization' }
),
settings: agentAuthorizationSettings,
},
];
return (
<>
<APMSettingsForm
vars={vars}
onChange={onChange}
isCloudPolicy={isCloudPolicy}
/>
<EuiSpacer />
<RUMSettingsForm vars={vars} onChange={onChange} />
<EuiSpacer />
<TLSSettingsForm vars={vars} onChange={onChange} />
{settingsSections.map((settingsSection) => {
return (
<React.Fragment key={settingsSection.id}>
<SettingsForm
settingsSection={settingsSection}
vars={vars}
onChange={handleFormChange}
/>
<EuiSpacer />
</React.Fragment>
);
})}
</>
);
}

View file

@ -1,197 +0,0 @@
/*
* 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 React from 'react';
import { getIntegerRt } from '../../../../../common/agent_configuration/runtime_types/integer_rt';
import { OnFormChangeFn, PackagePolicyVars } from '../typings';
import { SettingsForm } from './settings_form';
import { SettingDefinition } from './typings';
import { isSettingsFormValid, mergeNewVars, OPTIONAL_LABEL } from './utils';
const ENABLE_RUM_KEY = 'enable_rum';
const rumSettings: SettingDefinition[] = [
{
key: ENABLE_RUM_KEY,
type: 'boolean',
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.enableRumTitle',
{ defaultMessage: 'Enable RUM' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.enableRumDescription',
{ defaultMessage: 'Enable Real User Monitoring (RUM)' }
),
settings: [
{
key: 'rum_allow_headers',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderLabel',
{ defaultMessage: 'Allowed origin headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderHelpText',
{
defaultMessage: 'Allowed Origin headers to be sent by User Agents.',
}
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderTitle',
{ defaultMessage: 'Custom headers' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderDescription',
{ defaultMessage: 'Configure authentication for the agent' }
),
},
{
key: 'rum_allow_origins',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowOriginsLabel',
{ defaultMessage: 'Access-Control-Allow-Headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowOriginsHelpText',
{
defaultMessage:
'Supported Access-Control-Allow-Headers in addition to "Content-Type", "Content-Encoding" and "Accept".',
}
),
},
{
key: 'rum_response_headers',
type: 'area',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumResponseHeadersLabel',
{ defaultMessage: 'Custom HTTP response headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumResponseHeadersHelpText',
{
defaultMessage:
'Added to RUM responses, e.g. for security policy compliance.',
}
),
},
{
type: 'advanced_settings',
settings: [
{
key: 'rum_event_rate_limit',
type: 'integer',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitLabel',
{ defaultMessage: 'Rate limit events per IP' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitHelpText',
{
defaultMessage:
'Maximum number of events allowed per IP per second.',
}
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitTitle',
{ defaultMessage: 'Limits' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLimitDescription',
{ defaultMessage: 'Configure authentication for the agent' }
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'rum_event_rate_lru_size',
type: 'integer',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLRUSizeLabel',
{ defaultMessage: 'Rate limit cache size' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumEventRateLRUSizeHelpText',
{
defaultMessage:
'Number of unique IPs to be cached for the rate limiter.',
}
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'rum_library_pattern',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumLibraryPatternLabel',
{ defaultMessage: 'Library Frame Pattern' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumLibraryPatternHelpText',
{
defaultMessage:
"Identify library frames by matching a stacktrace frame's file_name and abs_path against this regexp.",
}
),
},
{
key: 'rum_allow_service_names',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowServiceNamesLabel',
{ defaultMessage: 'Allowed service names' }
),
labelAppend: OPTIONAL_LABEL,
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowServiceNamesTitle',
{ defaultMessage: 'Allowed service names' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowServiceNamesDescription',
{ defaultMessage: 'Configure authentication for the agent' }
),
},
],
},
],
},
];
interface Props {
vars: PackagePolicyVars;
onChange: OnFormChangeFn;
}
export function RUMSettingsForm({ vars, onChange }: Props) {
return (
<SettingsForm
title={i18n.translate(
'xpack.apm.fleet_integration.settings.rum.settings.title',
{ defaultMessage: 'Real User Monitoring' }
)}
subtitle={i18n.translate(
'xpack.apm.fleet_integration.settings.rum.settings.subtitle',
{ defaultMessage: 'Manage the configuration of the RUM JS agent.' }
)}
settings={rumSettings}
vars={vars}
onChange={(key, value) => {
const newVars = mergeNewVars(vars, key, value);
onChange(
newVars,
// only validates RUM when its flag is enabled
!newVars[ENABLE_RUM_KEY].value ||
isSettingsFormValid(rumSettings, newVars)
);
}}
/>
);
}

View file

@ -1,117 +0,0 @@
/*
* 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 React from 'react';
import { OnFormChangeFn, PackagePolicyVars } from '../typings';
import { SettingsForm } from './settings_form';
import { SettingDefinition } from './typings';
import {
isSettingsFormValid,
mergeNewVars,
OPTIONAL_LABEL,
REQUIRED_LABEL,
} from './utils';
const TLS_ENABLED_KEY = 'tls_enabled';
const tlsSettings: SettingDefinition[] = [
{
key: TLS_ENABLED_KEY,
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsEnabledTitle',
{ defaultMessage: 'Enable TLS' }
),
type: 'boolean',
settings: [
{
key: 'tls_certificate',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCertificateLabel',
{ defaultMessage: 'File path to server certificate' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCertificateTitle',
{ defaultMessage: 'TLS certificate' }
),
labelAppend: REQUIRED_LABEL,
required: true,
},
{
key: 'tls_key',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsKeyLabel',
{ defaultMessage: 'File path to server certificate key' }
),
labelAppend: REQUIRED_LABEL,
required: true,
},
{
key: 'tls_supported_protocols',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsSupportedProtocolsLabel',
{ defaultMessage: 'Supported protocol versions' }
),
labelAppend: OPTIONAL_LABEL,
},
{
key: 'tls_cipher_suites',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesLabel',
{ defaultMessage: 'Cipher suites for TLS connections' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesHelpText',
{ defaultMessage: 'Not configurable for TLS 1.3.' }
),
labelAppend: OPTIONAL_LABEL,
},
{
key: 'tls_curve_types',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCurveTypesLabel',
{ defaultMessage: 'Curve types for ECDHE based cipher suites' }
),
labelAppend: OPTIONAL_LABEL,
},
],
},
];
interface Props {
vars: PackagePolicyVars;
onChange: OnFormChangeFn;
}
export function TLSSettingsForm({ vars, onChange }: Props) {
return (
<SettingsForm
title={i18n.translate(
'xpack.apm.fleet_integration.settings.tls.settings.title',
{ defaultMessage: 'TLS Settings' }
)}
subtitle={i18n.translate(
'xpack.apm.fleet_integration.settings.tls.settings.subtitle',
{ defaultMessage: 'Settings for TLS certification.' }
)}
settings={tlsSettings}
vars={vars}
onChange={(key, value) => {
const newVars = mergeNewVars(vars, key, value);
onChange(
newVars,
// only validates TLS when its flag is enabled
!newVars[TLS_ENABLED_KEY].value ||
isSettingsFormValid(tlsSettings, newVars)
);
}}
/>
);
}

View file

@ -1,38 +0,0 @@
/*
* 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 * as t from 'io-ts';
export type SettingValidation = t.Type<any, string, unknown>;
interface AdvancedSettings {
type: 'advanced_settings';
settings: SettingDefinition[];
}
export interface Setting {
type:
| 'text'
| 'combo'
| 'area'
| 'boolean'
| 'integer'
| 'bytes'
| 'duration';
key: string;
rowTitle?: string;
rowDescription?: string;
label?: string;
helpText?: string;
placeholder?: string;
labelAppend?: string;
settings?: SettingDefinition[];
validation?: SettingValidation;
required?: boolean;
readOnly?: boolean;
}
export type SettingDefinition = Setting | AdvancedSettings;

View file

@ -0,0 +1,70 @@
/*
* 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 { getAgentAuthorizationSettings } from './agent_authorization_settings';
import { SettingsRow } from '../typings';
import { isSettingsFormValid } from '../settings_form/utils';
describe('apm-fleet-apm-integration', () => {
describe('getAgentAuthorizationSettings', () => {
function findSetting(key: string, settings: SettingsRow[]) {
return settings.find(
(setting) => setting.type !== 'advanced_setting' && setting.key === key
);
}
it('returns read only secret token when on cloud', () => {
const settings = getAgentAuthorizationSettings({ isCloudPolicy: true });
const secretToken = findSetting('secret_token', settings);
expect(secretToken).toEqual({
type: 'text',
key: 'secret_token',
readOnly: true,
labelAppend: 'Optional',
label: 'Secret token',
});
});
it('returns secret token when NOT on cloud', () => {
const settings = getAgentAuthorizationSettings({ isCloudPolicy: false });
const secretToken = findSetting('secret_token', settings);
expect(secretToken).toEqual({
type: 'text',
key: 'secret_token',
readOnly: false,
labelAppend: 'Optional',
label: 'Secret token',
});
});
});
describe('isAgentAuthorizationFormValid', () => {
describe('validates integer fields', () => {
[
'api_key_limit',
'anonymous_rate_limit_ip_limit',
'anonymous_rate_limit_event_limit',
].map((key) => {
it(`returns false when ${key} is lower than 1`, () => {
const settings = getAgentAuthorizationSettings({
isCloudPolicy: true,
});
expect(
isSettingsFormValid(settings, {
[key]: { value: 0, type: 'integer' },
})
).toBeFalsy();
expect(
isSettingsFormValid(settings, {
[key]: { value: -1, type: 'integer' },
})
).toBeFalsy();
});
});
});
});
});

View file

@ -0,0 +1,166 @@
/*
* 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 { getIntegerRt } from '../../../../../common/agent_configuration/runtime_types/integer_rt';
import { OPTIONAL_LABEL } from '../settings_form/utils';
import { SettingsRow } from '../typings';
export function getAgentAuthorizationSettings({
isCloudPolicy,
}: {
isCloudPolicy: boolean;
}): SettingsRow[] {
return [
{
type: 'boolean',
key: 'api_key_enabled',
labelAppend: OPTIONAL_LABEL,
placeholder: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.apiKeyAuthenticationPlaceholder',
{ defaultMessage: 'API key for agent authentication' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.apiKeyAuthenticationHelpText',
{
defaultMessage:
'Enable API Key auth between APM Server and APM Agents.',
}
),
settings: [
{
key: 'api_key_limit',
type: 'integer',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.apiKeyLimitLabel',
{ defaultMessage: 'Number of keys' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.apiKeyLimitHelpText',
{ defaultMessage: 'Might be used for security policy compliance.' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.apiKeyLimitTitle',
{
defaultMessage:
'Maximum number of API keys of Agent authentication',
}
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.apiKeyLimitDescription',
{
defaultMessage:
'Restrict number of unique API keys per minute, used for auth between APM Agents and Server.',
}
),
validation: getIntegerRt({ min: 1 }),
},
],
},
{
type: 'text',
key: 'secret_token',
readOnly: isCloudPolicy,
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.secretTokenLabel',
{ defaultMessage: 'Secret token' }
),
},
{
type: 'boolean',
key: 'anonymous_enabled',
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousEnabledTitle',
{ defaultMessage: 'Anonymous Agent access' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousEnabledHelpText',
{
defaultMessage:
'Enable anonymous access to APM Server for select APM Agents.',
}
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousEnabledDescription',
{
defaultMessage:
'Allow anonymous access only for specified agents and/or services. This is primarily intended to allow limited access for untrusted agents, such as Real User Monitoring. When anonymous auth is enabled, only agents matching the Allowed Agents and services matching the Allowed Services configuration are allowed. See below for details on default values.',
}
),
settings: [
{
type: 'combo',
key: 'anonymous_allow_agent',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentLabel',
{ defaultMessage: 'Allowed agents' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowAgentHelpText',
{
defaultMessage: 'Allowed agent names for anonymous access.',
}
),
},
{
type: 'combo',
key: 'anonymous_allow_service',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowServiceLabel',
{ defaultMessage: 'Allowed services' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousAllowServiceHelpText',
{
defaultMessage: 'Allowed service names for anonymous access.',
}
),
},
{
key: 'anonymous_rate_limit_ip_limit',
type: 'integer',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousRateLimitIpLimitLabel',
{ defaultMessage: 'Rate limit (IP limit)' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousRateLimitIpLimitHelpText',
{
defaultMessage:
'Number of unique client IPs for which a distinct rate limit will be maintained.',
}
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'anonymous_rate_limit_event_limit',
type: 'integer',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousRateLimitEventLimitLabel',
{
defaultMessage: 'Event rate limit (event limit)',
}
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.agentAuthorization.anonymousRateLimitEventLimitHelpText',
{
defaultMessage:
'Maximum number of events per client IP per second.',
}
),
validation: getIntegerRt({ min: 1 }),
},
],
},
];
}

View file

@ -0,0 +1,87 @@
/*
* 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 { getApmSettings } from './apm_settings';
import { SettingsRow, BasicSettingRow } from '../typings';
import { isSettingsFormValid } from '../settings_form/utils';
describe('apm_settings', () => {
describe('getApmSettings', () => {
function findSetting(key: string, settings: SettingsRow[]) {
return settings.find(
(setting) => setting.type !== 'advanced_setting' && setting.key === key
) as BasicSettingRow;
}
['host', 'url'].map((key) => {
it(`returns read only ${key} when on cloud`, () => {
const settings = getApmSettings({ isCloudPolicy: true });
const setting = findSetting(key, settings);
expect(setting.readOnly).toBeTruthy();
});
it(`returns ${key} when NOT on cloud`, () => {
const settings = getApmSettings({ isCloudPolicy: false });
const setting = findSetting(key, settings);
expect(setting.readOnly).toBeFalsy();
});
});
});
describe('isAPMFormValid', () => {
describe('validates integer fields', () => {
['max_header_bytes', 'max_event_bytes'].map((key) => {
it(`returns false when ${key} is lower than 1`, () => {
const settings = getApmSettings({ isCloudPolicy: true });
expect(
isSettingsFormValid(settings, {
[key]: { value: 0, type: 'integer' },
})
).toBeFalsy();
expect(
isSettingsFormValid(settings, {
[key]: { value: -1, type: 'integer' },
})
).toBeFalsy();
});
});
['max_connections'].map((key) => {
it(`returns false when ${key} is lower than 0`, () => {
const settings = getApmSettings({ isCloudPolicy: true });
expect(
isSettingsFormValid(settings, {
[key]: { value: -1, type: 'integer' },
})
).toBeFalsy();
});
});
});
describe('validates required fields', () => {
['host', 'url'].map((key) => {
it(`return false when ${key} is not defined`, () => {
const settings = getApmSettings({ isCloudPolicy: true });
expect(isSettingsFormValid(settings, {})).toBeFalsy();
});
});
});
describe('validates duration fields', () => {
['idle_timeout', 'read_timeout', 'shutdown_timeout', 'write_timeout'].map(
(key) => {
it(`return false when ${key} lower then 1ms`, () => {
const settings = getApmSettings({ isCloudPolicy: true });
expect(
isSettingsFormValid(settings, {
[key]: { value: '0ms', type: 'text' },
})
).toBeFalsy();
});
}
);
});
});
});

View file

@ -5,26 +5,16 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import { getDurationRt } from '../../../../../common/agent_configuration/runtime_types/duration_rt';
import { getIntegerRt } from '../../../../../common/agent_configuration/runtime_types/integer_rt';
import { OnFormChangeFn, PackagePolicyVars } from '../typings';
import { SettingsForm } from './settings_form';
import { SettingDefinition } from './typings';
import {
isSettingsFormValid,
mergeNewVars,
OPTIONAL_LABEL,
REQUIRED_LABEL,
} from './utils';
import { OPTIONAL_LABEL, REQUIRED_LABEL } from '../settings_form/utils';
import { SettingsRow } from '../typings';
interface Props {
vars: PackagePolicyVars;
onChange: OnFormChangeFn;
export function getApmSettings({
isCloudPolicy,
}: {
isCloudPolicy: boolean;
}
function getApmSettings(isCloudPolicy: boolean): SettingDefinition[] {
}): SettingsRow[] {
return [
{
type: 'text',
@ -63,33 +53,7 @@ function getApmSettings(isCloudPolicy: boolean): SettingDefinition[] {
required: true,
},
{
type: 'text',
key: 'secret_token',
readOnly: isCloudPolicy,
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.secretTokenLabel',
{ defaultMessage: 'Secret token' }
),
},
{
type: 'boolean',
key: 'api_key_enabled',
labelAppend: OPTIONAL_LABEL,
placeholder: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyAuthenticationPlaceholder',
{ defaultMessage: 'API key for agent authentication' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyAuthenticationHelpText',
{
defaultMessage:
'Enable API Key auth between APM Server and APM Agents.',
}
),
},
{
type: 'advanced_settings',
type: 'advanced_setting',
settings: [
{
key: 'max_header_bytes',
@ -176,7 +140,11 @@ function getApmSettings(isCloudPolicy: boolean): SettingDefinition[] {
'xpack.apm.fleet_integration.settings.apm.maxConnectionsLabel',
{ defaultMessage: 'Simultaneously accepted connections' }
),
validation: getIntegerRt({ min: 1 }),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.maxConnectionsHelpText',
{ defaultMessage: '0 for unlimited' }
),
validation: getIntegerRt({ min: 0 }),
},
{
key: 'response_headers',
@ -202,34 +170,6 @@ function getApmSettings(isCloudPolicy: boolean): SettingDefinition[] {
}
),
},
{
key: 'api_key_limit',
type: 'integer',
labelAppend: OPTIONAL_LABEL,
label: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitLabel',
{ defaultMessage: 'Number of keys' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitHelpText',
{ defaultMessage: 'Might be used for security policy compliance.' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitTitle',
{
defaultMessage:
'Maximum number of API keys of Agent authentication',
}
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.apm.apiKeyLimitDescription',
{
defaultMessage:
'Restrict number of unique API keys per minute, used for auth between aPM Agents and Server.',
}
),
validation: getIntegerRt({ min: 1 }),
},
{
key: 'capture_personal_data',
type: 'boolean',
@ -279,28 +219,3 @@ function getApmSettings(isCloudPolicy: boolean): SettingDefinition[] {
},
];
}
export function APMSettingsForm({ vars, onChange, isCloudPolicy }: Props) {
const apmSettings = useMemo(() => {
return getApmSettings(isCloudPolicy);
}, [isCloudPolicy]);
return (
<SettingsForm
title={i18n.translate(
'xpack.apm.fleet_integration.settings.apm.settings.title',
{ defaultMessage: 'General' }
)}
subtitle={i18n.translate(
'xpack.apm.fleet_integration.settings.apm.settings.subtitle',
{ defaultMessage: 'Settings for the APM integration.' }
)}
settings={apmSettings}
vars={vars}
onChange={(key, value) => {
const newVars = mergeNewVars(vars, key, value);
onChange(newVars, isSettingsFormValid(apmSettings, newVars));
}}
/>
);
}

View file

@ -0,0 +1,132 @@
/*
* 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 { PackagePolicyVars, SettingsRow } from '../typings';
import { isSettingsFormValid, OPTIONAL_LABEL } from '../settings_form/utils';
const ENABLE_RUM_KEY = 'enable_rum';
export function getRUMSettings(): SettingsRow[] {
return [
{
key: ENABLE_RUM_KEY,
type: 'boolean',
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.enableRumTitle',
{ defaultMessage: 'Enable RUM' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.enableRumDescription',
{ defaultMessage: 'Enable Real User Monitoring (RUM)' }
),
settings: [
{
key: 'rum_allow_origins',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowOriginsLabel',
{ defaultMessage: 'Origin Headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowOriginsHelpText',
{
defaultMessage:
'Allowed Origin headers to be sent by User Agents.',
}
),
},
{
key: 'rum_allow_headers',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderLabel',
{ defaultMessage: 'Access-Control-Allow-Headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderHelpText',
{
defaultMessage:
'Supported Access-Control-Allow-Headers in addition to "Content-Type", "Content-Encoding" and "Accept".',
}
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderTitle',
{ defaultMessage: 'Custom headers' }
),
rowDescription: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumAllowHeaderDescription',
{ defaultMessage: 'Configure authentication for the agent' }
),
},
{
key: 'rum_response_headers',
type: 'area',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumResponseHeadersLabel',
{ defaultMessage: 'Custom HTTP response headers' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumResponseHeadersHelpText',
{
defaultMessage:
'Added to RUM responses, e.g. for security policy compliance.',
}
),
},
{
type: 'advanced_setting',
settings: [
{
key: 'rum_library_pattern',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumLibraryPatternLabel',
{ defaultMessage: 'Library Frame Pattern' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumLibraryPatternHelpText',
{
defaultMessage:
"Identify library frames by matching a stacktrace frame's file_name and abs_path against this regexp.",
}
),
},
{
key: 'rum_exclude_from_grouping',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumExcludeFromGroupingLabel',
{ defaultMessage: 'Exclude from grouping' }
),
labelAppend: OPTIONAL_LABEL,
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.rum.rumExcludeFromGroupingHelpText',
{
defaultMessage:
"Exclude stacktrace frames from error group calculations by matching a stacktrace frame's `file_name` against this regexp.",
}
),
},
],
},
],
},
];
}
export function isRUMFormValid(
newVars: PackagePolicyVars,
rumSettings: SettingsRow[]
) {
// only validates RUM when its flag is enabled
return (
!newVars[ENABLE_RUM_KEY].value || isSettingsFormValid(rumSettings, newVars)
);
}

View file

@ -0,0 +1,36 @@
/*
* 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 { getTLSSettings, isTLSFormValid } from './tls_settings';
describe('tls_settings', () => {
describe('isTLSFormValid', () => {
describe('validates duration fields', () => {
['tls_certificate', 'tls_key'].map((key) => {
it(`return false when ${key} lower then 1ms`, () => {
const settings = getTLSSettings();
expect(
isTLSFormValid(
{ tls_enabled: { value: true, type: 'bool' } },
settings
)
).toBeFalsy();
});
});
});
it('returns true when tls_enabled is disabled', () => {
const settings = getTLSSettings();
expect(
isTLSFormValid(
{ tls_enabled: { value: false, type: 'bool' } },
settings
)
).toBeTruthy();
});
});
});

View file

@ -0,0 +1,95 @@
/*
* 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 { PackagePolicyVars, SettingsRow } from '../typings';
import {
isSettingsFormValid,
OPTIONAL_LABEL,
REQUIRED_LABEL,
} from '../settings_form/utils';
const TLS_ENABLED_KEY = 'tls_enabled';
export function getTLSSettings(): SettingsRow[] {
return [
{
key: TLS_ENABLED_KEY,
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsEnabledTitle',
{ defaultMessage: 'Enable TLS' }
),
type: 'boolean',
settings: [
{
key: 'tls_certificate',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCertificateLabel',
{ defaultMessage: 'File path to server certificate' }
),
rowTitle: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCertificateTitle',
{ defaultMessage: 'TLS certificate' }
),
labelAppend: REQUIRED_LABEL,
required: true,
},
{
key: 'tls_key',
type: 'text',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsKeyLabel',
{ defaultMessage: 'File path to server certificate key' }
),
labelAppend: REQUIRED_LABEL,
required: true,
},
{
key: 'tls_supported_protocols',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsSupportedProtocolsLabel',
{ defaultMessage: 'Supported protocol versions' }
),
labelAppend: OPTIONAL_LABEL,
},
{
key: 'tls_cipher_suites',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesLabel',
{ defaultMessage: 'Cipher suites for TLS connections' }
),
helpText: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCipherSuitesHelpText',
{ defaultMessage: 'Not configurable for TLS 1.3.' }
),
labelAppend: OPTIONAL_LABEL,
},
{
key: 'tls_curve_types',
type: 'combo',
label: i18n.translate(
'xpack.apm.fleet_integration.settings.tls.tlsCurveTypesLabel',
{ defaultMessage: 'Curve types for ECDHE based cipher suites' }
),
labelAppend: OPTIONAL_LABEL,
},
],
},
];
}
export function isTLSFormValid(
newVars: PackagePolicyVars,
tlsSettings: SettingsRow[]
) {
// only validates TLS when its flag is enabled
return (
!newVars[TLS_ENABLED_KEY].value || isSettingsFormValid(tlsSettings, newVars)
);
}

View file

@ -15,11 +15,11 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { FormRowOnChange } from './settings_form';
import { SettingDefinition } from './typings';
import { FormRowOnChange } from './';
import { SettingsRow } from '../typings';
interface Props {
setting: SettingDefinition;
row: SettingsRow;
value?: any;
onChange: FormRowOnChange;
}
@ -33,17 +33,15 @@ const DISABLED_LABEL = i18n.translate(
{ defaultMessage: 'Disabled' }
);
export function FormRowSetting({ setting, value, onChange }: Props) {
switch (setting.type) {
export function FormRowSetting({ row, value, onChange }: Props) {
switch (row.type) {
case 'boolean': {
return (
<EuiSwitch
label={
setting.placeholder || (value ? ENABLED_LABEL : DISABLED_LABEL)
}
label={row.placeholder || (value ? ENABLED_LABEL : DISABLED_LABEL)}
checked={value}
onChange={(e) => {
onChange(setting.key, e.target.checked);
onChange(row.key, e.target.checked);
}}
/>
);
@ -52,11 +50,11 @@ export function FormRowSetting({ setting, value, onChange }: Props) {
case 'text': {
return (
<EuiFieldText
readOnly={setting.readOnly}
readOnly={row.readOnly}
value={value}
prepend={setting.readOnly ? <EuiIcon type="lock" /> : undefined}
prepend={row.readOnly ? <EuiIcon type="lock" /> : undefined}
onChange={(e) => {
onChange(setting.key, e.target.value);
onChange(row.key, e.target.value);
}}
/>
);
@ -66,7 +64,7 @@ export function FormRowSetting({ setting, value, onChange }: Props) {
<EuiTextArea
value={value}
onChange={(e) => {
onChange(setting.key, e.target.value);
onChange(row.key, e.target.value);
}}
/>
);
@ -77,7 +75,7 @@ export function FormRowSetting({ setting, value, onChange }: Props) {
<EuiFieldNumber
value={value}
onChange={(e) => {
onChange(setting.key, e.target.value);
onChange(row.key, e.target.value);
}}
/>
);
@ -88,6 +86,7 @@ export function FormRowSetting({ setting, value, onChange }: Props) {
: [];
return (
<EuiComboBox
noSuggestions
placeholder={i18n.translate(
'xpack.apm.fleet_integration.settings.selectOrCreateOptions',
{ defaultMessage: 'Select or create options' }
@ -96,18 +95,18 @@ export function FormRowSetting({ setting, value, onChange }: Props) {
selectedOptions={comboOptions}
onChange={(option) => {
onChange(
setting.key,
row.key,
option.map(({ label }) => label)
);
}}
onCreateOption={(newOption) => {
onChange(setting.key, [...value, newOption]);
onChange(row.key, [...value, newOption]);
}}
isClearable={true}
/>
);
}
default:
throw new Error(`Unknown type "${setting.type}"`);
throw new Error(`Unknown type "${row.type}"`);
}
}

View file

@ -17,9 +17,8 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { PackagePolicyVars } from '../typings';
import { PackagePolicyVars, SettingsRow } from '../typings';
import { FormRowSetting } from './form_row_setting';
import { SettingDefinition } from './typings';
import { validateSettingValue } from './utils';
export type FormRowOnChange = (key: string, value: any) => void;
@ -29,73 +28,74 @@ function FormRow({
vars,
onChange,
}: {
initialSetting: SettingDefinition;
initialSetting: SettingsRow;
vars?: PackagePolicyVars;
onChange: FormRowOnChange;
}) {
function getSettingFormRow(setting: SettingDefinition) {
if (setting.type === 'advanced_settings') {
function getSettingFormRow(row: SettingsRow) {
if (row.type === 'advanced_setting') {
return (
<AdvancedOptions>
{setting.settings.map((advancedSetting) =>
{row.settings.map((advancedSetting) =>
getSettingFormRow(advancedSetting)
)}
</AdvancedOptions>
);
} else {
const { key } = setting;
const value = vars?.[key]?.value;
const { isValid, message } = validateSettingValue(setting, value);
return (
<React.Fragment key={key}>
<EuiDescribedFormGroup
title={<h3>{setting.rowTitle}</h3>}
description={setting.rowDescription}
>
<EuiFormRow
label={setting.label}
isInvalid={!isValid}
error={isValid ? undefined : message}
helpText={<EuiText size="xs">{setting.helpText}</EuiText>}
labelAppend={
<EuiText size="xs" color="subdued">
{setting.labelAppend}
</EuiText>
}
>
<FormRowSetting
setting={setting}
onChange={onChange}
value={value}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
{setting.settings &&
value &&
setting.settings.map((childSettings) =>
getSettingFormRow(childSettings)
)}
</React.Fragment>
);
}
const { key } = row;
const configEntry = vars?.[key];
// hides a field that doesn't have its key defined in vars.
// This is most likely to happen when a field is no longer supported in the current package version
if (!configEntry) {
return null;
}
const { value } = configEntry;
const { isValid, message } = validateSettingValue(row, value);
return (
<React.Fragment key={key}>
<EuiDescribedFormGroup
title={<h3>{row.rowTitle}</h3>}
description={row.rowDescription}
>
<EuiFormRow
label={row.label}
isInvalid={!isValid}
error={isValid ? undefined : message}
helpText={<EuiText size="xs">{row.helpText}</EuiText>}
labelAppend={
<EuiText size="xs" color="subdued">
{row.labelAppend}
</EuiText>
}
>
<FormRowSetting row={row} onChange={onChange} value={value} />
</EuiFormRow>
</EuiDescribedFormGroup>
{row.settings &&
value &&
row.settings.map((childSettings) => getSettingFormRow(childSettings))}
</React.Fragment>
);
}
return getSettingFormRow(initialSetting);
}
interface Props {
export interface SettingsSection {
id: string;
title: string;
subtitle: string;
settings: SettingDefinition[];
subtitle?: string;
settings: SettingsRow[];
}
interface Props {
settingsSection: SettingsSection;
vars?: PackagePolicyVars;
onChange: FormRowOnChange;
}
export function SettingsForm({
title,
subtitle,
settings,
vars,
onChange,
}: Props) {
export function SettingsForm({ settingsSection, vars, onChange }: Props) {
const { title, subtitle, settings } = settingsSection;
return (
<EuiPanel>
<EuiFlexGroup direction="column" gutterSize="s">
@ -104,11 +104,13 @@ export function SettingsForm({
<h3>{title}</h3>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<EuiText size="s" color="subdued">
{subtitle}
</EuiText>
</EuiFlexItem>
{subtitle && (
<EuiFlexItem>
<EuiText size="s" color="subdued">
{subtitle}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
<EuiHorizontalRule margin="s" />

View file

@ -5,8 +5,8 @@
* 2.0.
*/
import { getDurationRt } from '../../../../../common/agent_configuration/runtime_types/duration_rt';
import { PackagePolicyVars } from '../typings';
import { SettingDefinition } from './typings';
import { getIntegerRt } from '../../../../../common/agent_configuration/runtime_types/integer_rt';
import { PackagePolicyVars, SettingsRow } from '../typings';
import {
mergeNewVars,
isSettingsFormValid,
@ -16,7 +16,7 @@ import {
describe('settings utils', () => {
describe('validateSettingValue', () => {
it('returns invalid when setting is required and value is empty', () => {
const setting: SettingDefinition = {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
required: true,
@ -27,17 +27,17 @@ describe('settings utils', () => {
});
});
it('returns valid when setting is NOT required and value is empty', () => {
const setting: SettingDefinition = {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
};
expect(validateSettingValue(setting, undefined)).toEqual({
isValid: true,
message: '',
message: 'Required field',
});
});
it('returns valid when setting does not have a validation property', () => {
const setting: SettingDefinition = {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
};
@ -46,8 +46,8 @@ describe('settings utils', () => {
message: '',
});
});
it('returns valid after validating value', () => {
const setting: SettingDefinition = {
it('returns valid after validating duration value', () => {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
validation: getDurationRt({ min: '1ms' }),
@ -57,8 +57,8 @@ describe('settings utils', () => {
message: 'No errors!',
});
});
it('returns invalid after validating value', () => {
const setting: SettingDefinition = {
it('returns invalid after validating duration value', () => {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
validation: getDurationRt({ min: '1ms' }),
@ -68,9 +68,43 @@ describe('settings utils', () => {
message: 'Must be greater than 1ms',
});
});
it('returns valid after validating integer value', () => {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
validation: getIntegerRt({ min: 1 }),
};
expect(validateSettingValue(setting, 1)).toEqual({
isValid: true,
message: 'No errors!',
});
});
it('returns invalid after validating integer value', () => {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
validation: getIntegerRt({ min: 1 }),
};
expect(validateSettingValue(setting, 0)).toEqual({
isValid: false,
message: 'Must be greater than 1',
});
});
it('returns valid when required and value is empty', () => {
const setting: SettingsRow = {
key: 'foo',
type: 'text',
required: true,
};
expect(validateSettingValue(setting, '')).toEqual({
isValid: false,
message: 'Required field',
});
});
});
describe('isSettingsFormValid', () => {
const settings: SettingDefinition[] = [
const settings: SettingsRow[] = [
{ key: 'foo', type: 'text', required: true },
{
key: 'bar',
@ -79,7 +113,7 @@ describe('settings utils', () => {
},
{ key: 'baz', type: 'text', validation: getDurationRt({ min: '1ms' }) },
{
type: 'advanced_settings',
type: 'advanced_setting',
settings: [
{
type: 'text',

View file

@ -8,9 +8,8 @@
import { i18n } from '@kbn/i18n';
import { isRight } from 'fp-ts/lib/Either';
import { PathReporter } from 'io-ts/lib/PathReporter';
import { isEmpty } from 'lodash';
import { PackagePolicyVars } from '../typings';
import { SettingDefinition, Setting } from './typings';
import { isEmpty, isFinite } from 'lodash';
import { PackagePolicyVars, SettingsRow, BasicSettingRow } from '../typings';
export const REQUIRED_LABEL = i18n.translate(
'xpack.apm.fleet_integration.settings.requiredLabel',
@ -34,13 +33,13 @@ export function mergeNewVars(
}
export function isSettingsFormValid(
parentSettings: SettingDefinition[],
parentSettings: SettingsRow[],
vars: PackagePolicyVars
) {
function isSettingsValid(settings: SettingDefinition[]): boolean {
function isSettingsValid(settings: SettingsRow[]): boolean {
return !settings
.map((setting) => {
if (setting.type === 'advanced_settings') {
if (setting.type === 'advanced_setting') {
return isSettingsValid(setting.settings);
}
@ -59,11 +58,11 @@ export function isSettingsFormValid(
return isSettingsValid(parentSettings);
}
export function validateSettingValue(setting: Setting, value?: any) {
if (isEmpty(value)) {
export function validateSettingValue(setting: BasicSettingRow, value?: any) {
if (!isFinite(value) && isEmpty(value)) {
return {
isValid: !setting.required,
message: setting.required ? REQUIRED_FIELD : '',
message: REQUIRED_FIELD,
};
}

View file

@ -4,6 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { PackagePolicyConfigRecordEntry } from '../../../../../fleet/common';
export {
@ -19,7 +20,33 @@ export {
export type PackagePolicyVars = Record<string, PackagePolicyConfigRecordEntry>;
export type OnFormChangeFn = (
newVars: PackagePolicyVars,
isValid: boolean
) => void;
export type SettingValidation = t.Type<any, string, unknown>;
interface AdvancedSettingRow {
type: 'advanced_setting';
settings: SettingsRow[];
}
export interface BasicSettingRow {
type:
| 'text'
| 'combo'
| 'area'
| 'boolean'
| 'integer'
| 'bytes'
| 'duration';
key: string;
rowTitle?: string;
rowDescription?: string;
label?: string;
helpText?: string;
placeholder?: string;
labelAppend?: string;
settings?: SettingsRow[];
validation?: SettingValidation;
required?: boolean;
readOnly?: boolean;
}
export type SettingsRow = BasicSettingRow | AdvancedSettingRow;

View file

@ -72,7 +72,7 @@ function getFleetLink({
}
: {
label: GET_STARTED_WITH_FLEET_LABEL,
href: `${basePath}/app/integrations#/detail/apm-0.3.0/overview`,
href: `${basePath}/app/integrations#/detail/apm-0.4.0/overview`,
};
}

View file

@ -93,7 +93,7 @@ function TutorialFleetInstructions({ http, basePath, isDarkTheme }: Props) {
<EuiButton
iconType="analyzeEvent"
color="secondary"
href={`${basePath}/app/integrations#/detail/apm-0.3.0/overview`}
href={`${basePath}/app/integrations#/detail/apm-0.4.0/overview`}
>
{i18n.translate(
'xpack.apm.tutorial.apmServer.fleet.apmIntegration.button',

View file

@ -32,7 +32,7 @@ export function getApmPackagePolicyDefinition(
],
package: {
name: APM_PACKAGE_NAME,
version: '0.3.0',
version: '0.4.0',
title: 'Elastic APM',
},
};
@ -74,14 +74,6 @@ export const apmConfigMapping: Record<
type: 'text',
getValue: ({ cloudPluginSetup }) => cloudPluginSetup?.apm?.url,
},
'apm-server.secret_token': {
name: 'secret_token',
type: 'text',
},
'apm-server.api_key.enabled': {
name: 'api_key_enabled',
type: 'bool',
},
'apm-server.rum.enabled': {
name: 'enable_rum',
type: 'bool',
@ -90,10 +82,6 @@ export const apmConfigMapping: Record<
name: 'default_service_environment',
type: 'text',
},
'apm-server.rum.allow_service_names': {
name: 'rum_allow_service_names',
type: 'text',
},
'apm-server.rum.allow_origins': {
name: 'rum_allow_origins',
type: 'text',
@ -106,14 +94,6 @@ export const apmConfigMapping: Record<
name: 'rum_response_headers',
type: 'yaml',
},
'apm-server.rum.event_rate.limit': {
name: 'rum_event_rate_limit',
type: 'integer',
},
'apm-server.rum.event_rate.lru_size': {
name: 'rum_event_rate_lru_size',
type: 'integer',
},
'apm-server.rum.library_pattern': {
name: 'rum_library_pattern',
type: 'text',
@ -122,10 +102,6 @@ export const apmConfigMapping: Record<
name: 'rum_exclude_from_grouping',
type: 'text',
},
'apm-server.api_key.limit': {
name: 'api_key_limit',
type: 'integer',
},
'apm-server.max_event_size': {
name: 'max_event_bytes',
type: 'integer',
@ -190,4 +166,36 @@ export const apmConfigMapping: Record<
name: 'tls_curve_types',
type: 'text',
},
'apm-server.auth.secret_token': {
name: 'secret_token',
type: 'text',
},
'apm-server.auth.api_key.enabled': {
name: 'api_key_enabled',
type: 'bool',
},
'apm-server.auth.api_key.limit': {
name: 'api_key_limit',
type: 'bool',
},
'apm-server.auth.anonymous.enabled': {
name: 'anonymous_enabled',
type: 'bool',
},
'apm-server.auth.anonymous.allow_agent': {
name: 'anonymous_allow_agent',
type: 'text',
},
'apm-server.auth.anonymous.allow_service': {
name: 'anonymous_allow_service',
type: 'text',
},
'apm-server.auth.anonymous.rate_limit.ip_limit': {
name: 'anonymous_rate_limit_ip_limit',
type: 'integer',
},
'apm-server.auth.anonymous.rate_limit.event_limit': {
name: 'anonymous_rate_limit_event_limit',
type: 'integer',
},
};