diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index 0357fb698318a..3bde565c75ce6 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -101,7 +101,7 @@ describe('checking migration metadata changes on all registered SO types', () => "index-pattern": "48e77ca393c254e93256f11a7cdc0232dd754c08", "infrastructure-monitoring-log-view": "e2c78c1076bd35e57d7c5fa1b410e5c126d12327", "infrastructure-ui-source": "7c8dbbc0a608911f1b683a944f4a65383f6153ed", - "ingest-agent-policies": "54d586fdafae83ba326e47d1a3727b0d9c910a12", + "ingest-agent-policies": "a94bd53b8f81ca883de8a75386db04e27dda973e", "ingest-download-sources": "1e69dabd6db5e320fe08c5bda8f35f29bafc6b54", "ingest-outputs": "29181ecfdc7723f544325ecef7266bccbc691a54", "ingest-package-policies": "0335a28af793ce25b4969d2156cfaf1dae2ef812", diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 9b994df3afe33..9c4609e76adac 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -5807,6 +5807,24 @@ }, "agents": { "type": "number" + }, + "agent_features": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "name", + "enabled" + ] + } } }, "required": [ diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 65bab73457170..74440cfaf2d4f 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -3704,6 +3704,18 @@ components: type: number agents: type: number + agent_features: + type: array + items: + type: object + properties: + name: + type: string + enabled: + type: boolean + required: + - name + - enabled required: - id - status diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml index ab00144064760..76b6fba16c873 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/agent_policy.yaml @@ -35,6 +35,18 @@ allOf: type: number agents: type: number + agent_features: + type: array + items: + type: object + properties: + name: + type: string + enabled: + type: boolean + required: + - name + - enabled required: - id - status diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts index 4a2292a692172..cc0f0be82eae0 100644 --- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts @@ -32,6 +32,7 @@ export interface NewAgentPolicy { download_source_id?: string | null; fleet_server_host_id?: string | null; schema_version?: string; + agent_features?: Array<{ name: string; enabled: boolean }>; } export interface AgentPolicy extends Omit { @@ -112,6 +113,7 @@ export interface FullAgentPolicy { logs: boolean; }; download: { sourceURI: string }; + features: Record; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 3d1e28cc4f1d8..396478451d6ee 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -20,6 +20,10 @@ import { EuiSuperSelect, EuiToolTip, EuiBadge, + EuiRadioGroup, + EuiText, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -517,6 +521,89 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = /> + + + + } + description={ + + } + > + + + + + + + + + + + + + + + + + + + ), + }, + { + id: 'fqdn', + label: ( + + + + + + + + + + + + + + + ), + }, + ]} + idSelected={agentPolicy.agent_features?.length ? 'fqdn' : 'hostname'} + onChange={(id: string) => { + updateAgentPolicy({ + agent_features: id === 'hostname' ? [] : [{ name: 'fqdn', enabled: true }], + }); + }} + name="radio group" + /> + + {isEditing && 'id' in agentPolicy && !agentPolicy.is_managed ? ( + pick(agentPolicy, [ + 'name', + 'description', + 'namespace', + 'monitoring_enabled', + 'unenroll_timeout', + 'inactivity_timeout', + 'data_output_id', + 'monitoring_output_id', + 'download_source_id', + 'fleet_server_host_id', + 'agent_features', + ]); + const FormWrapper = styled.div` max-width: 800px; margin-right: auto; @@ -77,37 +92,10 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( const submitUpdateAgentPolicy = async () => { setIsLoading(true); try { - const { - name, - description, - namespace, - // eslint-disable-next-line @typescript-eslint/naming-convention - monitoring_enabled, - // eslint-disable-next-line @typescript-eslint/naming-convention - unenroll_timeout, - // eslint-disable-next-line @typescript-eslint/naming-convention - inactivity_timeout, - // eslint-disable-next-line @typescript-eslint/naming-convention - data_output_id, - // eslint-disable-next-line @typescript-eslint/naming-convention - monitoring_output_id, - // eslint-disable-next-line @typescript-eslint/naming-convention - download_source_id, - // eslint-disable-next-line @typescript-eslint/naming-convention - fleet_server_host_id, - } = agentPolicy; - const { data, error } = await sendUpdateAgentPolicy(agentPolicy.id, { - name, - description, - namespace, - monitoring_enabled, - unenroll_timeout, - inactivity_timeout, - data_output_id, - monitoring_output_id, - download_source_id, - fleet_server_host_id, - }); + const { data, error } = await sendUpdateAgentPolicy( + agentPolicy.id, + pickAgentPolicyKeysToSend(agentPolicy) + ); if (data) { notifications.toasts.addSuccess( i18n.translate('xpack.fleet.editAgentPolicy.successNotificationTitle', { @@ -141,19 +129,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>( () => generateUpdateAgentPolicyDevToolsRequest( agentPolicy.id, - pick( - agentPolicy, - 'name', - 'description', - 'namespace', - 'monitoring_enabled', - 'unenroll_timeout', - 'inactivity_timeout', - 'data_output_id', - 'monitoring_output_id', - 'download_source_id', - 'fleet_server_host_id' - ) + pickAgentPolicyKeysToSend(agentPolicy) ), [agentPolicy] ); diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 5ad423b2f1098..cd4ff53f86d46 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -113,6 +113,12 @@ const getSavedObjectTypes = ( monitoring_output_id: { type: 'keyword' }, download_source_id: { type: 'keyword' }, fleet_server_host_id: { type: 'keyword' }, + agent_features: { + properties: { + name: { type: 'keyword' }, + enabled: { type: 'boolean' }, + }, + }, }, }, migrations: { diff --git a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap index e4aee6aa84597..030dd4d9ec36f 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap +++ b/x-pack/plugins/fleet/server/services/agent_policies/__snapshots__/full_agent_policy.test.ts.snap @@ -6,6 +6,7 @@ Object { "download": Object { "sourceURI": "http://default-registry.co", }, + "features": Object {}, "monitoring": Object { "enabled": true, "logs": false, @@ -71,6 +72,7 @@ Object { "download": Object { "sourceURI": "http://default-registry.co", }, + "features": Object {}, "monitoring": Object { "enabled": true, "logs": false, @@ -136,6 +138,7 @@ Object { "download": Object { "sourceURI": "http://default-registry.co", }, + "features": Object {}, "monitoring": Object { "enabled": true, "logs": false, diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts index 352e831d12112..93c6b6dc891ea 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts @@ -388,6 +388,51 @@ describe('getFullAgentPolicy', () => { }, }); }); + + it('should add + transform agent features', async () => { + mockAgentPolicy({ + namespace: 'default', + revision: 1, + monitoring_enabled: ['metrics'], + agent_features: [ + { name: 'fqdn', enabled: true }, + { name: 'feature2', enabled: true }, + ], + }); + const agentPolicy = await getFullAgentPolicy(savedObjectsClientMock.create(), 'agent-policy'); + + expect(agentPolicy).toMatchObject({ + id: 'agent-policy', + outputs: { + default: { + type: 'elasticsearch', + hosts: ['http://127.0.0.1:9201'], + }, + }, + inputs: [], + revision: 1, + fleet: { + hosts: ['http://fleetserver:8220'], + }, + agent: { + monitoring: { + namespace: 'default', + use_output: 'default', + enabled: true, + logs: false, + metrics: true, + }, + features: { + fqdn: { + enabled: true, + }, + feature2: { + enabled: true, + }, + }, + }, + }); + }); }); describe('transformOutputToFullPolicyOutput', () => { diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts index 262ad2d64a606..fe434c015e438 100644 --- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.ts @@ -120,6 +120,10 @@ export async function getFullAgentPolicy( metrics: agentPolicy.monitoring_enabled.includes(dataTypes.Metrics), } : { enabled: false, logs: false, metrics: false }, + features: (agentPolicy.agent_features || []).reduce((acc, { name, ...featureConfig }) => { + acc[name] = featureConfig; + return acc; + }, {} as NonNullable['features']), }, }; diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts index 750613abfd21a..f85bd93b1e870 100644 --- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts @@ -39,6 +39,14 @@ export const AgentPolicyBaseSchema = { monitoring_output_id: schema.maybe(schema.nullable(schema.string())), download_source_id: schema.maybe(schema.nullable(schema.string())), fleet_server_host_id: schema.maybe(schema.nullable(schema.string())), + agent_features: schema.maybe( + schema.arrayOf( + schema.object({ + name: schema.string(), + enabled: schema.boolean(), + }) + ) + ), }; export const NewAgentPolicySchema = schema.object({ diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index c437197f9cafb..8df65c4152e1a 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -17,6 +17,7 @@ export default function (providerContext: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); + const es = getService('es'); describe('fleet_agent_policies', () => { skipIfNoDockerRegistry(providerContext); @@ -96,6 +97,67 @@ export default function (providerContext: FtrProviderContext) { expect(policy2.is_managed).to.equal(false); }); + it('does not allow arbitrary config in agent_features value', async () => { + await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'test-agent-features', + namespace: 'default', + agent_features: [ + { + name: 'fqdn', + enabled: true, + config: "I'm not allowed yet", + }, + ], + }) + .expect(400); + }); + + it('sets given agent_features value', async () => { + const { + body: { item: createdPolicy }, + } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'test-agent-features', + namespace: 'default', + agent_features: [ + { + name: 'fqdn', + enabled: true, + }, + ], + }) + .expect(200); + + const { body } = await supertest.get(`/api/fleet/agent_policies/${createdPolicy.id}`); + expect(body.item.agent_features).to.eql([ + { + name: 'fqdn', + enabled: true, + }, + ]); + + const policyDocRes = await es.search({ + index: '.fleet-policies', + query: { + term: { + policy_id: createdPolicy.id, + }, + }, + }); + + // @ts-expect-error + expect(policyDocRes?.hits?.hits[0]?._source?.data?.agent?.features).to.eql({ + fqdn: { + enabled: true, + }, + }); + }); + it('should return a 400 with an empty namespace', async () => { await supertest .post(`/api/fleet/agent_policies`)