mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
[Fleet] Add single agent migration endpoint (#220601)
## Summary Closes #217617 Adds a new endpoint `POST kbn:/api/fleet/agents/{agentId}/migrate` allowing a user to migrate a single agent to another cluster. Required parameters are: `enrollment_token` and `uri` - Adds `MIGRATE` as an action type and is reflected in UI - Includes unit tests as well as integration tests ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [ ] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ### Identify risks N/A # Release Note - Added endpoint allowing a user to migrate an individual agent to another cluster by specifying the URL and Enrollment Token. Note: tamper protected and fleet agents can not be migrated and attempting to do so will return a `403` status code. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
5b2fa54b4e
commit
ca2770f10f
19 changed files with 1238 additions and 5 deletions
|
@ -20181,7 +20181,8 @@
|
|||
"REQUEST_DIAGNOSTICS",
|
||||
"UPDATE_TAGS",
|
||||
"POLICY_CHANGE",
|
||||
"INPUT_ACTION"
|
||||
"INPUT_ACTION",
|
||||
"MIGRATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -22678,6 +22679,163 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/agents/{agentId}/migrate": {
|
||||
"post": {
|
||||
"description": "Migrate a single agent to another cluster.<br/><br/>[Required authorization] Route required privileges: fleet-agents-all.",
|
||||
"operationId": "post-fleet-agents-agentid-migrate",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A required header to protect against CSRF attacks",
|
||||
"in": "header",
|
||||
"name": "kbn-xsrf",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"example": "true",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "agentId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enrollment_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"settings": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ca_sha256": {
|
||||
"type": "string"
|
||||
},
|
||||
"certificate_authorities": {
|
||||
"type": "string"
|
||||
},
|
||||
"elastic_agent_cert": {
|
||||
"type": "string"
|
||||
},
|
||||
"elastic_agent_cert_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"elastic_agent_cert_key_passphrase": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"insecure": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"proxy_disabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"proxy_headers": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"proxy_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"replace_token": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"staging": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"uri": {
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"uri",
|
||||
"enrollment_token"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"actionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actionId"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"description": "Generic Error",
|
||||
"properties": {
|
||||
"attributes": {},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"errorType": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"statusCode": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"attributes"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Migrate a single agent",
|
||||
"tags": [
|
||||
"Elastic Agents"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/agents/{agentId}/reassign": {
|
||||
"post": {
|
||||
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
||||
|
|
|
@ -20181,7 +20181,8 @@
|
|||
"REQUEST_DIAGNOSTICS",
|
||||
"UPDATE_TAGS",
|
||||
"POLICY_CHANGE",
|
||||
"INPUT_ACTION"
|
||||
"INPUT_ACTION",
|
||||
"MIGRATE"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -22678,6 +22679,163 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/agents/{agentId}/migrate": {
|
||||
"post": {
|
||||
"description": "Migrate a single agent to another cluster.<br/><br/>[Required authorization] Route required privileges: fleet-agents-all.",
|
||||
"operationId": "post-fleet-agents-agentid-migrate",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A required header to protect against CSRF attacks",
|
||||
"in": "header",
|
||||
"name": "kbn-xsrf",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"example": "true",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "path",
|
||||
"name": "agentId",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enrollment_token": {
|
||||
"type": "string"
|
||||
},
|
||||
"settings": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ca_sha256": {
|
||||
"type": "string"
|
||||
},
|
||||
"certificate_authorities": {
|
||||
"type": "string"
|
||||
},
|
||||
"elastic_agent_cert": {
|
||||
"type": "string"
|
||||
},
|
||||
"elastic_agent_cert_key": {
|
||||
"type": "string"
|
||||
},
|
||||
"elastic_agent_cert_key_passphrase": {
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"insecure": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"proxy_disabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"proxy_headers": {
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"proxy_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"replace_token": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"staging": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tags": {
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"uri": {
|
||||
"format": "uri",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"uri",
|
||||
"enrollment_token"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"actionId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"actionId"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"additionalProperties": false,
|
||||
"description": "Generic Error",
|
||||
"properties": {
|
||||
"attributes": {},
|
||||
"error": {
|
||||
"type": "string"
|
||||
},
|
||||
"errorType": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"statusCode": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"attributes"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"summary": "Migrate a single agent",
|
||||
"tags": [
|
||||
"Elastic Agents"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/fleet/agents/{agentId}/reassign": {
|
||||
"post": {
|
||||
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
||||
|
|
|
@ -23811,6 +23811,109 @@ paths:
|
|||
summary: Create an agent action
|
||||
tags:
|
||||
- Elastic Agent actions
|
||||
/api/fleet/agents/{agentId}/migrate:
|
||||
post:
|
||||
description: 'Migrate a single agent to another cluster.<br/><br/>[Required authorization] Route required privileges: fleet-agents-all.'
|
||||
operationId: post-fleet-agents-agentid-migrate
|
||||
parameters:
|
||||
- description: A required header to protect against CSRF attacks
|
||||
in: header
|
||||
name: kbn-xsrf
|
||||
required: true
|
||||
schema:
|
||||
example: 'true'
|
||||
type: string
|
||||
- in: path
|
||||
name: agentId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
enrollment_token:
|
||||
type: string
|
||||
settings:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
ca_sha256:
|
||||
type: string
|
||||
certificate_authorities:
|
||||
type: string
|
||||
elastic_agent_cert:
|
||||
type: string
|
||||
elastic_agent_cert_key:
|
||||
type: string
|
||||
elastic_agent_cert_key_passphrase:
|
||||
type: string
|
||||
headers:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
insecure:
|
||||
type: boolean
|
||||
proxy_disabled:
|
||||
type: boolean
|
||||
proxy_headers:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
proxy_url:
|
||||
type: string
|
||||
replace_token:
|
||||
type: boolean
|
||||
staging:
|
||||
type: boolean
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
uri:
|
||||
format: uri
|
||||
type: string
|
||||
required:
|
||||
- uri
|
||||
- enrollment_token
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
actionId:
|
||||
type: string
|
||||
required:
|
||||
- actionId
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
description: Generic Error
|
||||
type: object
|
||||
properties:
|
||||
attributes: {}
|
||||
error:
|
||||
type: string
|
||||
errorType:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
statusCode:
|
||||
type: number
|
||||
required:
|
||||
- message
|
||||
- attributes
|
||||
summary: Migrate a single agent
|
||||
tags:
|
||||
- Elastic Agents
|
||||
/api/fleet/agents/{agentId}/reassign:
|
||||
post:
|
||||
description: '[Required authorization] Route required privileges: fleet-agents-all.'
|
||||
|
@ -24236,6 +24339,7 @@ paths:
|
|||
- UPDATE_TAGS
|
||||
- POLICY_CHANGE
|
||||
- INPUT_ACTION
|
||||
- MIGRATE
|
||||
type: string
|
||||
version:
|
||||
description: agent version number (UPGRADE action)
|
||||
|
|
|
@ -26053,6 +26053,109 @@ paths:
|
|||
summary: Create an agent action
|
||||
tags:
|
||||
- Elastic Agent actions
|
||||
/api/fleet/agents/{agentId}/migrate:
|
||||
post:
|
||||
description: 'Migrate a single agent to another cluster.<br/><br/>[Required authorization] Route required privileges: fleet-agents-all.'
|
||||
operationId: post-fleet-agents-agentid-migrate
|
||||
parameters:
|
||||
- description: A required header to protect against CSRF attacks
|
||||
in: header
|
||||
name: kbn-xsrf
|
||||
required: true
|
||||
schema:
|
||||
example: 'true'
|
||||
type: string
|
||||
- in: path
|
||||
name: agentId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
enrollment_token:
|
||||
type: string
|
||||
settings:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
ca_sha256:
|
||||
type: string
|
||||
certificate_authorities:
|
||||
type: string
|
||||
elastic_agent_cert:
|
||||
type: string
|
||||
elastic_agent_cert_key:
|
||||
type: string
|
||||
elastic_agent_cert_key_passphrase:
|
||||
type: string
|
||||
headers:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
insecure:
|
||||
type: boolean
|
||||
proxy_disabled:
|
||||
type: boolean
|
||||
proxy_headers:
|
||||
additionalProperties:
|
||||
type: string
|
||||
type: object
|
||||
proxy_url:
|
||||
type: string
|
||||
replace_token:
|
||||
type: boolean
|
||||
staging:
|
||||
type: boolean
|
||||
tags:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
uri:
|
||||
format: uri
|
||||
type: string
|
||||
required:
|
||||
- uri
|
||||
- enrollment_token
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
type: object
|
||||
properties:
|
||||
actionId:
|
||||
type: string
|
||||
required:
|
||||
- actionId
|
||||
'400':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
additionalProperties: false
|
||||
description: Generic Error
|
||||
type: object
|
||||
properties:
|
||||
attributes: {}
|
||||
error:
|
||||
type: string
|
||||
errorType:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
statusCode:
|
||||
type: number
|
||||
required:
|
||||
- message
|
||||
- attributes
|
||||
summary: Migrate a single agent
|
||||
tags:
|
||||
- Elastic Agents
|
||||
/api/fleet/agents/{agentId}/reassign:
|
||||
post:
|
||||
description: '[Required authorization] Route required privileges: fleet-agents-all.'
|
||||
|
@ -26478,6 +26581,7 @@ paths:
|
|||
- UPDATE_TAGS
|
||||
- POLICY_CHANGE
|
||||
- INPUT_ACTION
|
||||
- MIGRATE
|
||||
type: string
|
||||
version:
|
||||
description: agent version number (UPGRADE action)
|
||||
|
|
|
@ -152,6 +152,7 @@ export const AGENT_API_ROUTES = {
|
|||
CHECKIN_PATTERN: `${API_ROOT}/agents/{agentId}/checkin`,
|
||||
ACKS_PATTERN: `${API_ROOT}/agents/{agentId}/acks`,
|
||||
ACTIONS_PATTERN: `${API_ROOT}/agents/{agentId}/actions`,
|
||||
MIGRATE_PATTERN: `${API_ROOT}/agents/{agentId}/migrate`,
|
||||
CANCEL_ACTIONS_PATTERN: `${API_ROOT}/agents/actions/{actionId}/cancel`,
|
||||
UNENROLL_PATTERN: `${API_ROOT}/agents/{agentId}/unenroll`,
|
||||
BULK_UNENROLL_PATTERN: `${API_ROOT}/agents/bulk_unenroll`,
|
||||
|
|
|
@ -41,7 +41,8 @@ export type AgentActionType =
|
|||
| 'UPDATE_TAGS'
|
||||
| 'REQUEST_DIAGNOSTICS'
|
||||
| 'POLICY_CHANGE'
|
||||
| 'INPUT_ACTION';
|
||||
| 'INPUT_ACTION'
|
||||
| 'MIGRATE';
|
||||
|
||||
export type AgentUpgradeStateType =
|
||||
| 'UPG_REQUESTED'
|
||||
|
@ -60,6 +61,9 @@ export type FleetServerAgentComponentStatus = FleetServerAgentComponentStatusTup
|
|||
export interface NewAgentAction {
|
||||
type: AgentActionType;
|
||||
data?: any;
|
||||
enrollment_token?: string;
|
||||
target_uri?: string;
|
||||
additionalSettings?: string;
|
||||
ack_data?: any;
|
||||
sent_at?: string;
|
||||
agents: string[];
|
||||
|
|
|
@ -61,6 +61,11 @@ const actionNames: {
|
|||
completedText: 'input action completed',
|
||||
cancelledText: 'input action',
|
||||
},
|
||||
MIGRATE: {
|
||||
inProgressText: 'Migrating',
|
||||
completedText: 'migrated',
|
||||
cancelledText: 'migration',
|
||||
},
|
||||
ACTION: { inProgressText: 'Actioning', completedText: 'actioned', cancelledText: 'action' },
|
||||
};
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import {
|
|||
GetAgentsResponseSchema,
|
||||
GetAvailableAgentVersionsResponseSchema,
|
||||
ListAgentUploadsResponseSchema,
|
||||
MigrateSingleAgentResponseSchema,
|
||||
PostBulkActionResponseSchema,
|
||||
PostNewAgentActionResponseSchema,
|
||||
PostRetrieveAgentsByActionsResponseSchema,
|
||||
|
@ -56,6 +57,7 @@ import {
|
|||
|
||||
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
|
||||
|
||||
import { migrateSingleAgentHandler } from './migrate_handlers';
|
||||
jest.mock('./handlers', () => ({
|
||||
...jest.requireActual('./handlers'),
|
||||
getAgentHandler: jest.fn(),
|
||||
|
@ -75,6 +77,10 @@ jest.mock('./handlers', () => ({
|
|||
postRetrieveAgentsByActionsHandler: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./migrate_handlers', () => ({
|
||||
migrateSingleAgentHandler: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./actions_handlers', () => ({
|
||||
postNewAgentActionHandlerBuilder: jest.fn(),
|
||||
}));
|
||||
|
@ -476,4 +482,20 @@ describe('schema validation', () => {
|
|||
const validationResp = GetAvailableAgentVersionsResponseSchema.validate(expectedResponse);
|
||||
expect(validationResp).toEqual(expectedResponse);
|
||||
});
|
||||
|
||||
it('migrate single agent should return valid response', async () => {
|
||||
const expectedResponse = {
|
||||
actionId: 'migrate-action-123',
|
||||
};
|
||||
(migrateSingleAgentHandler as jest.Mock).mockImplementation((ctx, request, res) => {
|
||||
return res.ok({ body: expectedResponse });
|
||||
});
|
||||
await migrateSingleAgentHandler(context, {} as any, response);
|
||||
|
||||
expect(response.ok).toHaveBeenCalledWith({
|
||||
body: expectedResponse,
|
||||
});
|
||||
const validationResp = MigrateSingleAgentResponseSchema.validate(expectedResponse);
|
||||
expect(validationResp).toEqual(expectedResponse);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
GetTagsRequestSchema,
|
||||
GetOneAgentRequestSchema,
|
||||
UpdateAgentRequestSchema,
|
||||
MigrateSingleAgentRequestSchema,
|
||||
DeleteAgentRequestSchema,
|
||||
PostAgentUnenrollRequestSchema,
|
||||
PostBulkAgentUnenrollRequestSchema,
|
||||
|
@ -34,6 +35,7 @@ import {
|
|||
PostRetrieveAgentsByActionsRequestSchema,
|
||||
DeleteAgentUploadFileRequestSchema,
|
||||
GetTagsResponseSchema,
|
||||
MigrateSingleAgentResponseSchema,
|
||||
} from '../../types';
|
||||
import * as AgentService from '../../services/agents';
|
||||
import type { FleetConfigType } from '../..';
|
||||
|
@ -88,6 +90,7 @@ import {
|
|||
bulkRequestDiagnosticsHandler,
|
||||
requestDiagnosticsHandler,
|
||||
} from './request_diagnostics_handler';
|
||||
import { migrateSingleAgentHandler } from './migrate_handlers';
|
||||
|
||||
export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => {
|
||||
// Get one
|
||||
|
@ -123,6 +126,38 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT
|
|||
getAgentHandler
|
||||
);
|
||||
|
||||
// Migrate
|
||||
router.versioned
|
||||
.post({
|
||||
path: AGENT_API_ROUTES.MIGRATE_PATTERN,
|
||||
security: {
|
||||
authz: {
|
||||
requiredPrivileges: [FLEET_API_PRIVILEGES.AGENTS.ALL],
|
||||
},
|
||||
},
|
||||
summary: `Migrate a single agent`,
|
||||
description: `Migrate a single agent to another cluster.`,
|
||||
options: {
|
||||
tags: ['oas-tag:Elastic Agents'],
|
||||
},
|
||||
})
|
||||
.addVersion(
|
||||
{
|
||||
version: API_VERSIONS.public.v1,
|
||||
validate: {
|
||||
request: MigrateSingleAgentRequestSchema,
|
||||
response: {
|
||||
200: {
|
||||
body: () => MigrateSingleAgentResponseSchema,
|
||||
},
|
||||
400: {
|
||||
body: genericErrorResponse,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
migrateSingleAgentHandler
|
||||
);
|
||||
// Update
|
||||
router.versioned
|
||||
.put({
|
||||
|
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* 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 {
|
||||
httpServerMock,
|
||||
elasticsearchServiceMock,
|
||||
savedObjectsClientMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import type {
|
||||
KibanaResponseFactory,
|
||||
ElasticsearchClient,
|
||||
SavedObjectsClientContract,
|
||||
} from '@kbn/core/server';
|
||||
|
||||
import * as AgentService from '../../services/agents';
|
||||
|
||||
import { AgentNotFoundError, FleetUnauthorizedError } from '../../errors';
|
||||
|
||||
import { migrateSingleAgentHandler } from './migrate_handlers';
|
||||
|
||||
// Mock the agent service functions
|
||||
jest.mock('../../services/agents', () => {
|
||||
return {
|
||||
getAgentById: jest.fn(),
|
||||
getAgentPolicyForAgent: jest.fn(),
|
||||
migrateSingleAgent: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../services/app_context', () => {
|
||||
const { loggerMock } = jest.requireActual('@kbn/logging-mocks');
|
||||
return {
|
||||
appContextService: {
|
||||
getLogger: () => loggerMock.create(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('Migrate handlers', () => {
|
||||
describe('migrateSingleAgentHandler', () => {
|
||||
let mockResponse: jest.Mocked<KibanaResponseFactory>;
|
||||
|
||||
let mockRequest: any;
|
||||
let mockSavedObjectsClient: jest.Mocked<SavedObjectsClientContract>;
|
||||
let mockElasticsearchClient: jest.Mocked<ElasticsearchClient>;
|
||||
let mockContext: any;
|
||||
|
||||
const agentId = 'agent-id';
|
||||
const mockAgent = { id: agentId, components: [] };
|
||||
const mockAgentPolicy = { id: 'policy-id', is_protected: false };
|
||||
const mockSettings = { enrollment_token: 'token123', uri: 'https://example.com' };
|
||||
const mockActionResponse = { id: 'action-id' };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockResponse = httpServerMock.createResponseFactory();
|
||||
mockSavedObjectsClient = savedObjectsClientMock.create();
|
||||
mockElasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
|
||||
mockRequest = {
|
||||
params: { agentId },
|
||||
body: mockSettings,
|
||||
};
|
||||
|
||||
// Setup the context with correct structure
|
||||
mockContext = {
|
||||
core: {
|
||||
elasticsearch: {
|
||||
client: {
|
||||
asInternalUser: mockElasticsearchClient,
|
||||
},
|
||||
},
|
||||
savedObjects: {
|
||||
client: mockSavedObjectsClient,
|
||||
},
|
||||
},
|
||||
fleet: {},
|
||||
};
|
||||
|
||||
// Default mock returns
|
||||
(AgentService.getAgentById as jest.Mock).mockResolvedValue(mockAgent);
|
||||
(AgentService.getAgentPolicyForAgent as jest.Mock).mockResolvedValue(mockAgentPolicy);
|
||||
(AgentService.migrateSingleAgent as jest.Mock).mockResolvedValue({
|
||||
actionId: mockActionResponse.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls migrateSingleAgent with correct parameters and returns success', async () => {
|
||||
await migrateSingleAgentHandler(mockContext, mockRequest, mockResponse);
|
||||
|
||||
// Verify services were called with correct parameters
|
||||
expect(AgentService.getAgentById).toHaveBeenCalledWith(
|
||||
mockElasticsearchClient,
|
||||
mockSavedObjectsClient,
|
||||
agentId
|
||||
);
|
||||
|
||||
expect(AgentService.getAgentPolicyForAgent).toHaveBeenCalledWith(
|
||||
mockSavedObjectsClient,
|
||||
mockElasticsearchClient,
|
||||
agentId
|
||||
);
|
||||
|
||||
expect(AgentService.migrateSingleAgent).toHaveBeenCalledWith(
|
||||
mockElasticsearchClient,
|
||||
agentId,
|
||||
mockAgentPolicy,
|
||||
mockAgent,
|
||||
{
|
||||
...mockSettings,
|
||||
policyId: mockAgentPolicy.id,
|
||||
}
|
||||
);
|
||||
|
||||
// Verify response was returned correctly
|
||||
expect(mockResponse.ok).toHaveBeenCalledWith({
|
||||
body: { actionId: mockActionResponse.id },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns error when agent belongs to a protected policy', async () => {
|
||||
// Mock agent policy as protected
|
||||
(AgentService.getAgentPolicyForAgent as jest.Mock).mockResolvedValue({
|
||||
...mockAgentPolicy,
|
||||
is_protected: true,
|
||||
});
|
||||
// Change the migrateSingleAgent mock to be an error
|
||||
(AgentService.migrateSingleAgent as jest.Mock).mockRejectedValue(
|
||||
new FleetUnauthorizedError('Agent is protected and cannot be migrated')
|
||||
);
|
||||
await expect(
|
||||
migrateSingleAgentHandler(mockContext, mockRequest, mockResponse)
|
||||
).rejects.toThrow('Agent is protected and cannot be migrated');
|
||||
});
|
||||
|
||||
it('returns error when agent is a fleet-server agent', async () => {
|
||||
// Mock agent as fleet-server agent
|
||||
(AgentService.getAgentById as jest.Mock).mockResolvedValue({
|
||||
...mockAgent,
|
||||
components: [{ type: 'fleet-server' }],
|
||||
});
|
||||
// Change the migrateSingleAgent mock to be an error
|
||||
(AgentService.migrateSingleAgent as jest.Mock).mockRejectedValue(
|
||||
new FleetUnauthorizedError('Agent is protected and cannot be migrated')
|
||||
);
|
||||
await expect(
|
||||
migrateSingleAgentHandler(mockContext, mockRequest, mockResponse)
|
||||
).rejects.toThrow('Agent is protected and cannot be migrated');
|
||||
});
|
||||
|
||||
it('returns error when agent is not found', async () => {
|
||||
const agentError = new AgentNotFoundError('Agent not found');
|
||||
(AgentService.getAgentById as jest.Mock).mockRejectedValue(agentError);
|
||||
await expect(
|
||||
migrateSingleAgentHandler(mockContext, mockRequest, mockResponse)
|
||||
).rejects.toThrow(agentError.message);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { TypeOf } from '@kbn/config-schema';
|
||||
|
||||
import type { FleetRequestHandler, MigrateSingleAgentRequestSchema } from '../../types';
|
||||
import * as AgentService from '../../services/agents';
|
||||
|
||||
export const migrateSingleAgentHandler: FleetRequestHandler<
|
||||
TypeOf<typeof MigrateSingleAgentRequestSchema.params>,
|
||||
undefined,
|
||||
TypeOf<typeof MigrateSingleAgentRequestSchema.body>
|
||||
> = async (context, request, response) => {
|
||||
const [coreContext] = await Promise.all([context.core, context.fleet]);
|
||||
const esClient = coreContext.elasticsearch.client.asInternalUser;
|
||||
const soClient = coreContext.savedObjects.client;
|
||||
const options = request.body;
|
||||
// First validate the agent exists
|
||||
const agent = await AgentService.getAgentById(esClient, soClient, request.params.agentId);
|
||||
// Using the agent id, get the agent policy
|
||||
const agentPolicy = await AgentService.getAgentPolicyForAgent(
|
||||
soClient,
|
||||
esClient,
|
||||
request.params.agentId
|
||||
);
|
||||
|
||||
const body = await AgentService.migrateSingleAgent(
|
||||
esClient,
|
||||
request.params.agentId,
|
||||
agentPolicy,
|
||||
agent,
|
||||
{
|
||||
...options,
|
||||
policyId: agentPolicy?.id,
|
||||
}
|
||||
);
|
||||
return response.ok({ body });
|
||||
};
|
|
@ -69,6 +69,9 @@ export async function createAgentAction(
|
|||
traceparent: apm.currentTraceparent,
|
||||
is_automatic: newAgentAction.is_automatic,
|
||||
policyId: newAgentAction.policyId,
|
||||
enrollment_token: newAgentAction.enrollment_token,
|
||||
target_uri: newAgentAction.target_uri,
|
||||
settings: newAgentAction.additionalSettings ? newAgentAction.additionalSettings : undefined,
|
||||
};
|
||||
|
||||
const messageSigningService = appContextService.getMessageSigningService();
|
||||
|
@ -79,7 +82,6 @@ export async function createAgentAction(
|
|||
signature: signedBody.signature,
|
||||
};
|
||||
}
|
||||
|
||||
await esClient.create({
|
||||
index: AGENT_ACTIONS_INDEX,
|
||||
id: uuidv4(),
|
||||
|
|
|
@ -15,6 +15,7 @@ export * from './reassign';
|
|||
export * from './update_agent_tags';
|
||||
export * from './action_status';
|
||||
export * from './request_diagnostics';
|
||||
export * from './migrate';
|
||||
export { getAgentUploads, getAgentUploadFile, deleteAgentUploadFile } from './uploads';
|
||||
export { AgentServiceImpl } from './agent_service';
|
||||
export type { AgentClient, AgentService } from './agent_service';
|
||||
|
|
|
@ -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 { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import type { AgentPolicy, Agent } from '../../types';
|
||||
|
||||
import { migrateSingleAgent } from './migrate';
|
||||
import { createAgentAction } from './actions';
|
||||
|
||||
// Mock the imported functions
|
||||
jest.mock('./actions');
|
||||
|
||||
const mockedCreateAgentAction = createAgentAction as jest.MockedFunction<typeof createAgentAction>;
|
||||
|
||||
const mockedAgent: Agent = {
|
||||
id: 'agent-123',
|
||||
policy_id: 'policy-456',
|
||||
last_checkin: new Date().toISOString(),
|
||||
components: [],
|
||||
local_metadata: {
|
||||
elastic: {
|
||||
agent: {
|
||||
version: '1.0.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
enrolled_at: new Date().toISOString(),
|
||||
active: true,
|
||||
packages: [],
|
||||
type: 'PERMANENT',
|
||||
};
|
||||
const mockedPolicy: AgentPolicy = {
|
||||
id: 'policy-456',
|
||||
is_protected: false,
|
||||
status: 'active',
|
||||
is_managed: false,
|
||||
updated_at: new Date().toISOString(),
|
||||
updated_by: 'kibana',
|
||||
revision: 1,
|
||||
name: 'Test Policy',
|
||||
namespace: 'default',
|
||||
};
|
||||
|
||||
describe('Agent migration', () => {
|
||||
let esClientMock: ReturnType<typeof elasticsearchServiceMock.createInternalClient>;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset mocks before each test
|
||||
jest.resetAllMocks();
|
||||
esClientMock = elasticsearchServiceMock.createInternalClient();
|
||||
|
||||
// Mock the createAgentAction response
|
||||
mockedCreateAgentAction.mockResolvedValue({
|
||||
id: 'test-action-id',
|
||||
type: 'MIGRATE',
|
||||
agents: ['agent-123'],
|
||||
created_at: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('migrateSingleAgent', () => {
|
||||
it('should create a MIGRATE action for the specified agent', async () => {
|
||||
const agentId = 'agent-123';
|
||||
const options = {
|
||||
policyId: 'policy-456',
|
||||
enrollment_token: 'test-enrollment-token',
|
||||
uri: 'https://test-fleet-server.example.com',
|
||||
settings: { timeout: 300 },
|
||||
};
|
||||
|
||||
const result = await migrateSingleAgent(
|
||||
esClientMock,
|
||||
agentId,
|
||||
mockedPolicy,
|
||||
mockedAgent,
|
||||
options
|
||||
);
|
||||
|
||||
// Verify createAgentAction was called with correct params
|
||||
expect(mockedCreateAgentAction).toHaveBeenCalledTimes(1);
|
||||
expect(mockedCreateAgentAction).toHaveBeenCalledWith(esClientMock, {
|
||||
agents: [agentId],
|
||||
created_at: expect.any(String),
|
||||
type: 'MIGRATE',
|
||||
policyId: options.policyId,
|
||||
enrollment_token: options.enrollment_token,
|
||||
target_uri: options.uri,
|
||||
additionalSettings: options.settings,
|
||||
});
|
||||
|
||||
// Verify result contains the action ID from createAgentAction
|
||||
expect(result).toEqual({ actionId: 'test-action-id' });
|
||||
});
|
||||
|
||||
it('should handle empty additional settings', async () => {
|
||||
const agentId = 'agent-123';
|
||||
const options = {
|
||||
policyId: 'policy-456',
|
||||
enrollment_token: 'test-enrollment-token',
|
||||
uri: 'https://test-fleet-server.example.com',
|
||||
};
|
||||
|
||||
await migrateSingleAgent(esClientMock, agentId, mockedPolicy, mockedAgent, options);
|
||||
|
||||
// Verify createAgentAction was called with correct params and undefined additionalSettings
|
||||
expect(mockedCreateAgentAction).toHaveBeenCalledWith(
|
||||
esClientMock,
|
||||
expect.objectContaining({
|
||||
additionalSettings: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the agent is protected', async () => {
|
||||
const agentId = 'agent-123';
|
||||
const options = {
|
||||
policyId: 'policy-456',
|
||||
enrollment_token: 'test-enrollment-token',
|
||||
uri: 'https://test-fleet-server.example.com',
|
||||
};
|
||||
mockedPolicy.is_protected = true;
|
||||
await expect(
|
||||
migrateSingleAgent(esClientMock, agentId, mockedPolicy, mockedAgent, options)
|
||||
).rejects.toThrowError('Agent is protected and cannot be migrated');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 type { ElasticsearchClient } from '@kbn/core/server';
|
||||
|
||||
import { FleetUnauthorizedError } from '../../errors';
|
||||
|
||||
import type { AgentPolicy, Agent } from '../../types';
|
||||
|
||||
import { createAgentAction } from './actions';
|
||||
|
||||
export async function migrateSingleAgent(
|
||||
esClient: ElasticsearchClient,
|
||||
agentId: string,
|
||||
agentPolicy: AgentPolicy | undefined,
|
||||
agent: Agent,
|
||||
options: any
|
||||
) {
|
||||
// If the agent belongs to a policy that is protected or has fleet-server as a component meaning its a fleet server agent, throw an error
|
||||
if (agentPolicy?.is_protected || agent.components?.some((c) => c.type === 'fleet-server')) {
|
||||
throw new FleetUnauthorizedError(`Agent is protected and cannot be migrated`);
|
||||
}
|
||||
const response = await createAgentAction(esClient, {
|
||||
agents: [agentId],
|
||||
created_at: new Date().toISOString(),
|
||||
type: 'MIGRATE',
|
||||
policyId: options.policyId,
|
||||
enrollment_token: options.enrollment_token,
|
||||
target_uri: options.uri,
|
||||
additionalSettings: options.settings,
|
||||
});
|
||||
return { actionId: response.id };
|
||||
}
|
|
@ -86,7 +86,21 @@ export const GetAgentsRequestSchema = {
|
|||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const MigrateOptionsSchema = {
|
||||
ca_sha256: schema.maybe(schema.string()),
|
||||
certificate_authorities: schema.maybe(schema.string()),
|
||||
elastic_agent_cert: schema.maybe(schema.string()),
|
||||
elastic_agent_cert_key: schema.maybe(schema.string()),
|
||||
elastic_agent_cert_key_passphrase: schema.maybe(schema.string()),
|
||||
headers: schema.maybe(schema.recordOf(schema.string(), schema.string())),
|
||||
insecure: schema.maybe(schema.boolean()),
|
||||
proxy_disabled: schema.maybe(schema.boolean()),
|
||||
proxy_headers: schema.maybe(schema.recordOf(schema.string(), schema.string())),
|
||||
proxy_url: schema.maybe(schema.string()),
|
||||
staging: schema.maybe(schema.boolean()),
|
||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
replace_token: schema.maybe(schema.boolean()),
|
||||
};
|
||||
export const AgentComponentStateSchema = schema.oneOf([
|
||||
schema.literal('STARTING'),
|
||||
schema.literal('CONFIGURING'),
|
||||
|
@ -523,6 +537,19 @@ export const UpdateAgentRequestSchema = {
|
|||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
}),
|
||||
};
|
||||
export const MigrateSingleAgentRequestSchema = {
|
||||
params: schema.object({
|
||||
agentId: schema.string(),
|
||||
}),
|
||||
body: schema.object({
|
||||
uri: schema.uri(),
|
||||
enrollment_token: schema.string(),
|
||||
settings: schema.maybe(schema.object(MigrateOptionsSchema)),
|
||||
}),
|
||||
};
|
||||
export const MigrateSingleAgentResponseSchema = schema.object({
|
||||
actionId: schema.string(),
|
||||
});
|
||||
|
||||
export const PostBulkUpdateAgentTagsRequestSchema = {
|
||||
body: schema.object({
|
||||
|
@ -657,6 +684,7 @@ export const GetActionStatusResponseSchema = schema.object({
|
|||
schema.literal('UPDATE_TAGS'),
|
||||
schema.literal('POLICY_CHANGE'),
|
||||
schema.literal('INPUT_ACTION'),
|
||||
schema.literal('MIGRATE'),
|
||||
]),
|
||||
nbAgentsActioned: schema.number({
|
||||
meta: {
|
||||
|
|
|
@ -27,5 +27,6 @@ export default function loadTests({ loadTestFile, getService }) {
|
|||
loadTestFile(require.resolve('./uploads'));
|
||||
loadTestFile(require.resolve('./get_agents_by_actions'));
|
||||
loadTestFile(require.resolve('./privileges'));
|
||||
loadTestFile(require.resolve('./migrate'));
|
||||
});
|
||||
}
|
||||
|
|
226
x-pack/test/fleet_api_integration/apis/agents/migrate.ts
Normal file
226
x-pack/test/fleet_api_integration/apis/agents/migrate.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* 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 { AGENTS_INDEX } from '@kbn/fleet-plugin/common';
|
||||
|
||||
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
const { getService } = providerContext;
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
|
||||
describe('fleet_agents_migrate', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/fleet/agents');
|
||||
|
||||
// Create agent policies using the Fleet API
|
||||
// Policy 1 - regular policy without tamper protection
|
||||
const policy1Response = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
name: 'Policy 1',
|
||||
namespace: 'default',
|
||||
description: 'Test policy 1',
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const policy1 = policy1Response.body.item;
|
||||
|
||||
// Policy 2 - with tamper protection
|
||||
const policy2Response = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
name: 'Policy 2',
|
||||
namespace: 'default',
|
||||
description: 'Test policy 2 with tamper protection',
|
||||
monitoring_enabled: ['logs', 'metrics'],
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const policy2 = policy2Response.body.item;
|
||||
|
||||
// First, install the endpoint package which is required for the endpoint package policy
|
||||
await supertest
|
||||
.post('/api/fleet/epm/packages/endpoint')
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({ force: true })
|
||||
.expect(200);
|
||||
|
||||
// Fetch the installed package to get its current version
|
||||
const packageInfoResponse = await supertest
|
||||
.get('/api/fleet/epm/packages/endpoint')
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.expect(200);
|
||||
|
||||
const endpointPackageVersion = packageInfoResponse.body.item.version;
|
||||
|
||||
// Create Elastic Defend package policy for policy2 with proper configuration
|
||||
await supertest
|
||||
.post(`/api/fleet/package_policies`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
name: 'endpoint-1',
|
||||
description: 'Endpoint Security Integration',
|
||||
namespace: 'default',
|
||||
policy_id: policy2.id,
|
||||
enabled: true,
|
||||
inputs: [
|
||||
{
|
||||
type: 'endpoint',
|
||||
enabled: true,
|
||||
streams: [],
|
||||
config: {
|
||||
policy: {
|
||||
value: {
|
||||
windows: {
|
||||
events: {
|
||||
dll_and_driver_load: true,
|
||||
dns: true,
|
||||
file: true,
|
||||
network: true,
|
||||
process: true,
|
||||
registry: true,
|
||||
security: true,
|
||||
},
|
||||
malware: { mode: 'prevent' },
|
||||
ransomware: { mode: 'prevent' },
|
||||
memory_protection: { mode: 'prevent' },
|
||||
behavior_protection: { mode: 'prevent' },
|
||||
popup: {
|
||||
malware: { enabled: true, message: '' },
|
||||
ransomware: { enabled: true, message: '' },
|
||||
},
|
||||
},
|
||||
mac: {
|
||||
events: { file: true, network: true, process: true },
|
||||
malware: { mode: 'prevent' },
|
||||
behavior_protection: { mode: 'prevent' },
|
||||
popup: { malware: { enabled: true, message: '' } },
|
||||
},
|
||||
linux: {
|
||||
events: { file: true, network: true, process: true },
|
||||
malware: { mode: 'prevent' },
|
||||
behavior_protection: { mode: 'prevent' },
|
||||
popup: { malware: { enabled: true, message: '' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
package: {
|
||||
name: 'endpoint',
|
||||
title: 'Elastic Defend',
|
||||
version: endpointPackageVersion, // Use the actual installed version
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Now enable tamper protection on policy2
|
||||
await supertest
|
||||
.put(`/api/fleet/agent_policies/${policy2.id}`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
name: policy2.name,
|
||||
namespace: 'default',
|
||||
description: policy2.description,
|
||||
is_protected: true, // Enable tamper protection
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// Create agents in Elasticsearch
|
||||
await es.index({
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
id: 'agent1',
|
||||
document: {
|
||||
policy_id: policy1.id,
|
||||
},
|
||||
});
|
||||
|
||||
await es.index({
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
id: 'agent2',
|
||||
document: {
|
||||
policy_id: policy2.id, // Policy 2 is tamper protected
|
||||
},
|
||||
});
|
||||
await es.index({
|
||||
refresh: 'wait_for',
|
||||
index: AGENTS_INDEX,
|
||||
id: 'agent3',
|
||||
document: {
|
||||
policy_id: policy1.id,
|
||||
components: [
|
||||
{
|
||||
type: 'fleet-server',
|
||||
id: 'fleet-server',
|
||||
revision: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/fleet/agents');
|
||||
// Cleanup will be handled automatically by Fleet API
|
||||
});
|
||||
|
||||
describe('POST /agents/{agentId}/migrate', () => {
|
||||
it('should return a 200 if the migration action is successful', async () => {
|
||||
const {} = await supertest
|
||||
.post(`/api/fleet/agents/agent1/migrate`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
enrollment_token: '1234',
|
||||
uri: 'https://example.com',
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('should return a 403 if the agent is tamper protected', async () => {
|
||||
const {} = await supertest
|
||||
.post(`/api/fleet/agents/agent2/migrate`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
enrollment_token: '1234',
|
||||
uri: 'https://example.com',
|
||||
})
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should return a 403 if the agent is a fleet-agent', async () => {
|
||||
const {} = await supertest
|
||||
.post(`/api/fleet/agents/agent3/migrate`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
enrollment_token: '1234',
|
||||
uri: 'https://example.com',
|
||||
})
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it('should return a 404 when agent does not exist', async () => {
|
||||
await supertest
|
||||
.post(`/api/fleet/agents/agent100/migrate`)
|
||||
.set('kbn-xsrf', 'xx')
|
||||
.send({
|
||||
enrollment_token: '1234',
|
||||
uri: 'https://example.com',
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -314,6 +314,17 @@ export default function (providerContext: FtrProviderContext) {
|
|||
beforeEach: updateAgentBeforeEach,
|
||||
afterEach: updateAgentAfterEach,
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/api/fleet/agents/agent1/migrate',
|
||||
scenarios: ALL_SCENARIOS,
|
||||
send: {
|
||||
enrollment_token: '1234',
|
||||
uri: 'https://example.com',
|
||||
},
|
||||
beforeEach: updateAgentBeforeEach,
|
||||
afterEach: updateAgentAfterEach,
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/api/fleet/agents/agent1',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue