[8.x] [Fleet] Send Agentless API resources (#206042) (#207701)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[Fleet] Send Agentless API resources
(#206042)](https://github.com/elastic/kibana/pull/206042)

<!--- Backport version: 9.6.4 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Amir Ben
Nun","email":"34831306+amirbenun@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-01-19T10:52:10Z","message":"[Fleet]
Send Agentless API resources (#206042)\n\n## Summary\n\nConclude
agentless policy resources and send them to the Agentless API\non the
creation request.\n- Resolves:
https://github.com/elastic/kibana/issues/203371","sha":"fec5d743984b384d48ceb077e1f840cb98b5a16e","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport
missing","Team:Fleet","v9.0.0","Team:Cloud
Security","backport:prev-minor","ci:project-deploy-security"],"title":"[Fleet]
Send Agentless API
resources","number":206042,"url":"https://github.com/elastic/kibana/pull/206042","mergeCommit":{"message":"[Fleet]
Send Agentless API resources (#206042)\n\n## Summary\n\nConclude
agentless policy resources and send them to the Agentless API\non the
creation request.\n- Resolves:
https://github.com/elastic/kibana/issues/203371","sha":"fec5d743984b384d48ceb077e1f840cb98b5a16e"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/206042","number":206042,"mergeCommit":{"message":"[Fleet]
Send Agentless API resources (#206042)\n\n## Summary\n\nConclude
agentless policy resources and send them to the Agentless API\non the
creation request.\n- Resolves:
https://github.com/elastic/kibana/issues/203371","sha":"fec5d743984b384d48ceb077e1f840cb98b5a16e"}}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Amir Ben Nun 2025-01-22 15:40:07 +02:00 committed by GitHub
parent 3fd9c4d886
commit 747e766250
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 591 additions and 3 deletions

View file

@ -9578,6 +9578,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
agents:
type: number
data_output_id:
@ -10113,6 +10129,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
data_output_id:
nullable: true
type: string
@ -10292,6 +10324,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
agents:
type: number
data_output_id:
@ -10846,6 +10894,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
agents:
type: number
data_output_id:
@ -11380,6 +11444,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
agents:
type: number
data_output_id:
@ -11914,6 +11994,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
data_output_id:
nullable: true
type: string
@ -12093,6 +12189,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
agents:
type: number
data_output_id:
@ -12647,6 +12759,22 @@ paths:
- name
- enabled
type: array
agentless:
additionalProperties: false
type: object
properties:
resources:
additionalProperties: false
type: object
properties:
requests:
additionalProperties: false
type: object
properties:
cpu:
type: string
memory:
type: string
agents:
type: number
data_output_id:

View file

@ -481,6 +481,7 @@
"agent_features",
"agent_features.enabled",
"agent_features.name",
"agentless",
"data_output_id",
"description",
"download_source_id",
@ -601,6 +602,7 @@
"agent_features",
"agent_features.enabled",
"agent_features.name",
"agentless",
"data_output_id",
"description",
"download_source_id",

View file

@ -1624,6 +1624,10 @@
}
}
},
"agentless": {
"dynamic": false,
"properties": {}
},
"data_output_id": {
"type": "keyword"
},
@ -1994,6 +1998,10 @@
}
}
},
"agentless": {
"dynamic": false,
"properties": {}
},
"data_output_id": {
"type": "keyword"
},

View file

@ -105,7 +105,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"file": "6b65ae5899b60ebe08656fd163ea532e557d3c98",
"file-upload-usage-collection-telemetry": "06e0a8c04f991e744e09d03ab2bd7f86b2088200",
"fileShare": "5be52de1747d249a221b5241af2838264e19aaa1",
"fleet-agent-policies": "908765a33aab066f4ac09446686b2d884aceed00",
"fleet-agent-policies": "4a5c6477d2a61121e95ea9865ed1403a28c38706",
"fleet-fleet-server-host": "69be15f6b6f2a2875ad3c7050ddea7a87f505417",
"fleet-message-signing-keys": "93421f43fed2526b59092a4e3c65d64bc2266c0f",
"fleet-package-policies": "0206c20f27286787b91814a2e7872f06dc1e8e47",
@ -121,7 +121,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": "c1818c4119259908875b4c777ae62b11ba0585cd",
"ingest-agent-policies": "57ebfb047cf0b81c6fa0ceed8586fa7199c7c5e2",
"ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d",
"ingest-outputs": "daafff49255ab700e07491376fe89f04fc998b91",
"ingest-package-policies": "60d43f475f91417d14d9df05476acf2e63e99435",

View file

@ -44,6 +44,7 @@ export interface NewAgentPolicy {
keep_monitoring_alive?: boolean | null;
supports_agentless?: boolean | null;
global_data_tags?: GlobalDataTag[];
agentless?: AgentlessPolicy;
monitoring_pprof_enabled?: boolean;
monitoring_http?: {
enabled?: boolean;
@ -63,6 +64,15 @@ export interface NewAgentPolicy {
};
}
export interface AgentlessPolicy {
resources?: {
requests?: {
memory?: string;
cpu?: string;
};
};
}
export interface GlobalDataTag {
name: string;
value: string | number;

View file

@ -201,6 +201,12 @@ export interface DeploymentsModesAgentless extends DeploymentsModesDefault {
organization?: string;
division?: string;
team?: string;
resources?: {
requests: {
cpu: string;
memory: string;
};
};
}
export interface DeploymentsModes {
agentless: DeploymentsModesAgentless;

View file

@ -125,6 +125,24 @@ describe('useSetupTechnology', () => {
inactivity_timeout: 3600,
};
const packageInfoWithoutAgentless = {
policy_templates: [
{
name: 'cspm',
title: 'Template 1',
description: '',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: false,
},
},
},
] as RegistryPolicyTemplate[],
} as PackageInfo;
const packageInfoMock = {
policy_templates: [
{
@ -140,6 +158,40 @@ describe('useSetupTechnology', () => {
organization: 'org',
division: 'div',
team: 'team',
resources: {
requests: {
memory: '256Mi',
cpu: '100m',
},
},
},
},
},
{
name: 'not-cspm',
title: 'Template 2',
description: '',
deployment_modes: {
default: {
enabled: true,
},
},
},
] as RegistryPolicyTemplate[],
} as PackageInfo;
const packageInfoWithoutResources = {
policy_templates: [
{
name: 'cspm',
title: 'Template 1',
description: '',
deployment_modes: {
default: {
enabled: true,
},
agentless: {
enabled: true,
},
},
},
@ -473,6 +525,14 @@ describe('useSetupTechnology', () => {
{ name: 'division', value: 'div' },
{ name: 'team', value: 'team' },
],
agentless: {
resources: {
requests: {
memory: '256Mi',
cpu: '100m',
},
},
},
});
expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true });
});
@ -488,6 +548,133 @@ describe('useSetupTechnology', () => {
});
});
it('should have agentless resources section on the request when creating agentless policy with resources', async () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
(useStartServices as MockFn).mockReturnValue({
cloud: {
isCloudEnabled: true,
},
});
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
packageInfo: packageInfoMock,
updatePackagePolicy: updatePackagePolicyMock,
})
);
act(() => {
result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS);
});
await waitFor(() => {
expect(setNewAgentPolicy).toHaveBeenCalledWith(
expect.objectContaining({
agentless: {
resources: {
requests: {
memory: '256Mi',
cpu: '100m',
},
},
},
})
);
});
});
it('should not have agentless section on the request when creating agentless policy without resources', async () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
(useStartServices as MockFn).mockReturnValue({
cloud: {
isCloudEnabled: true,
},
});
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
packageInfo: packageInfoWithoutResources,
updatePackagePolicy: updatePackagePolicyMock,
})
);
act(() => {
result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS);
});
await waitFor(() => {
expect(setNewAgentPolicy).toHaveBeenCalledWith(
expect.not.objectContaining({
agentless: {},
})
);
});
});
it('should not have agentless section on the request when creating policy with agentless disabled', async () => {
(useConfig as MockFn).mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'https://agentless.api.url',
},
},
} as any);
(useStartServices as MockFn).mockReturnValue({
cloud: {
isCloudEnabled: true,
},
});
const { result } = renderHook(() =>
useSetupTechnology({
setNewAgentPolicy,
newAgentPolicy: newAgentPolicyMock,
updateAgentPolicies: updateAgentPoliciesMock,
setSelectedPolicyTab: setSelectedPolicyTabMock,
packagePolicy: packagePolicyMock,
packageInfo: packageInfoWithoutAgentless,
updatePackagePolicy: updatePackagePolicyMock,
})
);
act(() => {
result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS);
});
await waitFor(() => {
expect(setNewAgentPolicy).toHaveBeenCalledWith(
expect.not.objectContaining({
agentless: {},
})
);
});
});
it('should have global_data_tags with the integration team when creating agentless policy with global_data_tags', async () => {
(useConfig as MockFn).mockReturnValue({
agentless: {

View file

@ -129,6 +129,12 @@ export function useSetupTechnology({
name: agentlessPolicyName,
global_data_tags: getGlobaDataTags(packageInfo),
};
const agentlessPolicy = getAgentlessPolicy(packageInfo);
if (agentlessPolicy) {
nextNewAgentlessPolicy.agentless = agentlessPolicy;
}
setCurrentAgentPolicy(nextNewAgentlessPolicy);
setNewAgentPolicy(nextNewAgentlessPolicy as NewAgentPolicy);
updateAgentPolicies([nextNewAgentlessPolicy] as AgentPolicy[]);
@ -204,3 +210,26 @@ const getGlobaDataTags = (packageInfo?: PackageInfo) => {
},
];
};
const getAgentlessPolicy = (packageInfo?: PackageInfo) => {
if (
!packageInfo?.policy_templates &&
!packageInfo?.policy_templates?.some((policy) => policy.deployment_modes)
) {
return;
}
const agentlessPolicyTemplate = packageInfo.policy_templates.find(
(policy) => policy.deployment_modes
);
// assumes that all the policy templates agentless deployments modes indentify have the same organization, division and team
const agentlessInfo = agentlessPolicyTemplate?.deployment_modes?.agentless;
if (!agentlessInfo?.resources) {
return;
}
return {
resources: agentlessInfo.resources,
};
};

View file

@ -246,6 +246,10 @@ export const getSavedObjectTypes = (
advanced_settings: { type: 'flattened', index: false },
supports_agentless: { type: 'boolean' },
global_data_tags: { type: 'flattened', index: false },
agentless: {
dynamic: false,
properties: {},
},
monitoring_pprof_enabled: { type: 'boolean', index: false },
monitoring_http: { type: 'flattened', index: false },
monitoring_diagnostics: { type: 'flattened', index: false },
@ -313,6 +317,19 @@ export const getSavedObjectTypes = (
},
],
},
'6': {
changes: [
{
type: 'mappings_addition',
addedMappings: {
agentless: {
dynamic: false,
properties: {},
},
},
},
],
},
},
},
[AGENT_POLICY_SAVED_OBJECT_TYPE]: {
@ -357,6 +374,10 @@ export const getSavedObjectTypes = (
advanced_settings: { type: 'flattened', index: false },
supports_agentless: { type: 'boolean' },
global_data_tags: { type: 'flattened', index: false },
agentless: {
dynamic: false,
properties: {},
},
},
},
modelVersions: {

View file

@ -809,6 +809,7 @@ class AgentPolicyService {
'fleet_server_host_id',
'supports_agentless',
'global_data_tags',
'agentless',
'monitoring_pprof_enabled',
'monitoring_http',
'monitoring_diagnostics',

View file

@ -285,6 +285,119 @@ describe('Agentless Agent service', () => {
);
});
it('should create agentless agent with resources', async () => {
const returnValue = {
id: 'mocked',
regional_id: 'mocked',
};
(axios as jest.MockedFunction<typeof axios>).mockResolvedValueOnce(returnValue);
const soClient = getAgentPolicyCreateMock();
// ignore unrelated unique name constraint
const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
jest.spyOn(appContextService, 'getConfig').mockReturnValue({
agentless: {
enabled: true,
api: {
url: 'http://api.agentless.com',
tls: {
certificate: '/path/to/cert',
key: '/path/to/key',
ca: '/path/to/ca',
},
},
},
} as any);
jest
.spyOn(appContextService, 'getCloud')
.mockReturnValue({ isCloudEnabled: true, isServerlessEnabled: true } as any);
jest
.spyOn(appContextService, 'getKibanaVersion')
.mockReturnValue('mocked-kibana-version-infinite');
mockedListFleetServerHosts.mockResolvedValue({
items: [
{
id: 'mocked-fleet-server-id',
host: 'http://fleetserver:8220',
active: true,
is_default: true,
host_urls: ['http://fleetserver:8220'],
},
],
} as any);
mockedListEnrollmentApiKeys.mockResolvedValue({
items: [
{
id: 'mocked-fleet-enrollment-token-id',
policy_id: 'mocked-fleet-enrollment-policy-id',
api_key: 'mocked-fleet-enrollment-api-key',
},
],
} as any);
const createAgentlessAgentReturnValue = await agentlessAgentService.createAgentlessAgent(
esClient,
soClient,
{
id: 'mocked-agentless-agent-policy-id',
name: 'agentless agent policy',
namespace: 'default',
supports_agentless: true,
agentless: {
resources: {
requests: {
memory: '1Gi',
cpu: '500m',
},
},
},
global_data_tags: [
{
name: 'organization',
value: 'elastic',
},
{
name: 'division',
value: 'cloud',
},
{
name: 'team',
value: 'fleet',
},
],
} as AgentPolicy
);
expect(axios).toHaveBeenCalledTimes(1);
expect(createAgentlessAgentReturnValue).toEqual(returnValue);
expect(axios).toHaveBeenCalledWith(
expect.objectContaining({
data: {
fleet_token: 'mocked-fleet-enrollment-api-key',
fleet_url: 'http://fleetserver:8220',
policy_id: 'mocked-agentless-agent-policy-id',
resources: {
requests: {
memory: '1Gi',
cpu: '500m',
},
},
labels: {
owner: {
org: 'elastic',
division: 'cloud',
team: 'fleet',
},
},
},
headers: expect.anything(),
httpsAgent: expect.anything(),
method: 'POST',
url: 'http://api.agentless.com/api/v1/serverless/deployments',
})
);
});
it('should create agentless agent when no labels are given', async () => {
const returnValue = {
id: 'mocked',

View file

@ -110,6 +110,7 @@ class AgentlessAgentService {
policy_id: policyId,
fleet_url: fleetUrl,
fleet_token: fleetToken,
resources: agentlessAgentPolicy.agentless?.resources,
labels,
},
method: 'POST',

View file

@ -4,7 +4,7 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import type { GlobalDataTag } from '../../../common/types';
import type { AgentlessPolicy, GlobalDataTag } from '../../../common/types';
import { AgentPolicyBaseSchema } from './agent_policy';
@ -91,5 +91,56 @@ describe('AgentPolicyBaseSchema', () => {
AgentPolicyBaseSchema.global_data_tags.validate(tags);
}).not.toThrow();
});
it('should not throw an error if provided with empty agentless resources', () => {
const agentless: AgentlessPolicy = {};
expect(() => {
AgentPolicyBaseSchema.agentless.validate(agentless);
}).not.toThrow();
});
it('should not throw an error if provided with valid agentless resources', () => {
const agentless: AgentlessPolicy = {
resources: {
requests: {
memory: '1Gi',
cpu: '1',
},
},
};
expect(() => {
AgentPolicyBaseSchema.agentless.validate(agentless);
}).not.toThrow();
});
it('should throw an error if provided with invalid agentless memory', () => {
const agentless: AgentlessPolicy = {
resources: {
requests: {
memory: '1',
},
},
};
expect(() => {
AgentPolicyBaseSchema.agentless.validate(agentless);
}).toThrow();
});
it('should throw an error if provided with invalid agentless CPU', () => {
const agentless: AgentlessPolicy = {
resources: {
requests: {
cpu: '1CPU',
},
},
};
expect(() => {
AgentPolicyBaseSchema.agentless.validate(agentless);
}).toThrow();
});
});
});

View file

@ -39,6 +39,20 @@ function isInteger(n: number) {
}
}
const memoryRegex = /^\d+(Mi|Gi)$/;
function validateMemory(s: string) {
if (!memoryRegex.test(s)) {
return 'Invalid memory format';
}
}
const cpuRegex = /^(\d+m|\d+(\.\d+)?)$/;
function validateCPU(s: string) {
if (!cpuRegex.test(s)) {
return 'Invalid CPU format';
}
}
export const AgentPolicyBaseSchema = {
id: schema.maybe(schema.string()),
space_ids: schema.maybe(schema.arrayOf(schema.string())),
@ -102,6 +116,20 @@ export const AgentPolicyBaseSchema = {
}
)
),
agentless: schema.maybe(
schema.object({
resources: schema.maybe(
schema.object({
requests: schema.maybe(
schema.object({
memory: schema.maybe(schema.string({ validate: validateMemory })),
cpu: schema.maybe(schema.string({ validate: validateCPU })),
})
),
})
),
})
),
monitoring_pprof_enabled: schema.maybe(schema.boolean()),
monitoring_http: schema.maybe(
schema.object({

View file

@ -17,6 +17,7 @@ import type {
KafkaConnectionTypeType,
AgentUpgradeDetails,
OutputPreset,
AgentlessPolicy,
} from '../../common/types';
import type { AgentType, FleetServerAgentComponent } from '../../common/types/models';
@ -65,6 +66,7 @@ export interface AgentPolicySOAttributes {
agents?: number;
overrides?: any | null;
global_data_tags?: Array<{ name: string; value: string | number }>;
agentless?: AgentlessPolicy;
version?: string;
}

View file

@ -122,6 +122,7 @@ const getAgentPolicyDataForUpdate = (
'download_source_id',
'fleet_server_host_id',
'global_data_tags',
'agentless',
'has_fleet_server',
'id',
'inactivity_timeout',