mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 11:05:39 -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",
|
"REQUEST_DIAGNOSTICS",
|
||||||
"UPDATE_TAGS",
|
"UPDATE_TAGS",
|
||||||
"POLICY_CHANGE",
|
"POLICY_CHANGE",
|
||||||
"INPUT_ACTION"
|
"INPUT_ACTION",
|
||||||
|
"MIGRATE"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"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": {
|
"/api/fleet/agents/{agentId}/reassign": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
||||||
|
|
|
@ -20181,7 +20181,8 @@
|
||||||
"REQUEST_DIAGNOSTICS",
|
"REQUEST_DIAGNOSTICS",
|
||||||
"UPDATE_TAGS",
|
"UPDATE_TAGS",
|
||||||
"POLICY_CHANGE",
|
"POLICY_CHANGE",
|
||||||
"INPUT_ACTION"
|
"INPUT_ACTION",
|
||||||
|
"MIGRATE"
|
||||||
],
|
],
|
||||||
"type": "string"
|
"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": {
|
"/api/fleet/agents/{agentId}/reassign": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
"description": "[Required authorization] Route required privileges: fleet-agents-all.",
|
||||||
|
|
|
@ -23811,6 +23811,109 @@ paths:
|
||||||
summary: Create an agent action
|
summary: Create an agent action
|
||||||
tags:
|
tags:
|
||||||
- Elastic Agent actions
|
- 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:
|
/api/fleet/agents/{agentId}/reassign:
|
||||||
post:
|
post:
|
||||||
description: '[Required authorization] Route required privileges: fleet-agents-all.'
|
description: '[Required authorization] Route required privileges: fleet-agents-all.'
|
||||||
|
@ -24236,6 +24339,7 @@ paths:
|
||||||
- UPDATE_TAGS
|
- UPDATE_TAGS
|
||||||
- POLICY_CHANGE
|
- POLICY_CHANGE
|
||||||
- INPUT_ACTION
|
- INPUT_ACTION
|
||||||
|
- MIGRATE
|
||||||
type: string
|
type: string
|
||||||
version:
|
version:
|
||||||
description: agent version number (UPGRADE action)
|
description: agent version number (UPGRADE action)
|
||||||
|
|
|
@ -26053,6 +26053,109 @@ paths:
|
||||||
summary: Create an agent action
|
summary: Create an agent action
|
||||||
tags:
|
tags:
|
||||||
- Elastic Agent actions
|
- 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:
|
/api/fleet/agents/{agentId}/reassign:
|
||||||
post:
|
post:
|
||||||
description: '[Required authorization] Route required privileges: fleet-agents-all.'
|
description: '[Required authorization] Route required privileges: fleet-agents-all.'
|
||||||
|
@ -26478,6 +26581,7 @@ paths:
|
||||||
- UPDATE_TAGS
|
- UPDATE_TAGS
|
||||||
- POLICY_CHANGE
|
- POLICY_CHANGE
|
||||||
- INPUT_ACTION
|
- INPUT_ACTION
|
||||||
|
- MIGRATE
|
||||||
type: string
|
type: string
|
||||||
version:
|
version:
|
||||||
description: agent version number (UPGRADE action)
|
description: agent version number (UPGRADE action)
|
||||||
|
|
|
@ -152,6 +152,7 @@ export const AGENT_API_ROUTES = {
|
||||||
CHECKIN_PATTERN: `${API_ROOT}/agents/{agentId}/checkin`,
|
CHECKIN_PATTERN: `${API_ROOT}/agents/{agentId}/checkin`,
|
||||||
ACKS_PATTERN: `${API_ROOT}/agents/{agentId}/acks`,
|
ACKS_PATTERN: `${API_ROOT}/agents/{agentId}/acks`,
|
||||||
ACTIONS_PATTERN: `${API_ROOT}/agents/{agentId}/actions`,
|
ACTIONS_PATTERN: `${API_ROOT}/agents/{agentId}/actions`,
|
||||||
|
MIGRATE_PATTERN: `${API_ROOT}/agents/{agentId}/migrate`,
|
||||||
CANCEL_ACTIONS_PATTERN: `${API_ROOT}/agents/actions/{actionId}/cancel`,
|
CANCEL_ACTIONS_PATTERN: `${API_ROOT}/agents/actions/{actionId}/cancel`,
|
||||||
UNENROLL_PATTERN: `${API_ROOT}/agents/{agentId}/unenroll`,
|
UNENROLL_PATTERN: `${API_ROOT}/agents/{agentId}/unenroll`,
|
||||||
BULK_UNENROLL_PATTERN: `${API_ROOT}/agents/bulk_unenroll`,
|
BULK_UNENROLL_PATTERN: `${API_ROOT}/agents/bulk_unenroll`,
|
||||||
|
|
|
@ -41,7 +41,8 @@ export type AgentActionType =
|
||||||
| 'UPDATE_TAGS'
|
| 'UPDATE_TAGS'
|
||||||
| 'REQUEST_DIAGNOSTICS'
|
| 'REQUEST_DIAGNOSTICS'
|
||||||
| 'POLICY_CHANGE'
|
| 'POLICY_CHANGE'
|
||||||
| 'INPUT_ACTION';
|
| 'INPUT_ACTION'
|
||||||
|
| 'MIGRATE';
|
||||||
|
|
||||||
export type AgentUpgradeStateType =
|
export type AgentUpgradeStateType =
|
||||||
| 'UPG_REQUESTED'
|
| 'UPG_REQUESTED'
|
||||||
|
@ -60,6 +61,9 @@ export type FleetServerAgentComponentStatus = FleetServerAgentComponentStatusTup
|
||||||
export interface NewAgentAction {
|
export interface NewAgentAction {
|
||||||
type: AgentActionType;
|
type: AgentActionType;
|
||||||
data?: any;
|
data?: any;
|
||||||
|
enrollment_token?: string;
|
||||||
|
target_uri?: string;
|
||||||
|
additionalSettings?: string;
|
||||||
ack_data?: any;
|
ack_data?: any;
|
||||||
sent_at?: string;
|
sent_at?: string;
|
||||||
agents: string[];
|
agents: string[];
|
||||||
|
|
|
@ -61,6 +61,11 @@ const actionNames: {
|
||||||
completedText: 'input action completed',
|
completedText: 'input action completed',
|
||||||
cancelledText: 'input action',
|
cancelledText: 'input action',
|
||||||
},
|
},
|
||||||
|
MIGRATE: {
|
||||||
|
inProgressText: 'Migrating',
|
||||||
|
completedText: 'migrated',
|
||||||
|
cancelledText: 'migration',
|
||||||
|
},
|
||||||
ACTION: { inProgressText: 'Actioning', completedText: 'actioned', cancelledText: 'action' },
|
ACTION: { inProgressText: 'Actioning', completedText: 'actioned', cancelledText: 'action' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {
|
||||||
GetAgentsResponseSchema,
|
GetAgentsResponseSchema,
|
||||||
GetAvailableAgentVersionsResponseSchema,
|
GetAvailableAgentVersionsResponseSchema,
|
||||||
ListAgentUploadsResponseSchema,
|
ListAgentUploadsResponseSchema,
|
||||||
|
MigrateSingleAgentResponseSchema,
|
||||||
PostBulkActionResponseSchema,
|
PostBulkActionResponseSchema,
|
||||||
PostNewAgentActionResponseSchema,
|
PostNewAgentActionResponseSchema,
|
||||||
PostRetrieveAgentsByActionsResponseSchema,
|
PostRetrieveAgentsByActionsResponseSchema,
|
||||||
|
@ -56,6 +57,7 @@ import {
|
||||||
|
|
||||||
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
|
import { postNewAgentActionHandlerBuilder } from './actions_handlers';
|
||||||
|
|
||||||
|
import { migrateSingleAgentHandler } from './migrate_handlers';
|
||||||
jest.mock('./handlers', () => ({
|
jest.mock('./handlers', () => ({
|
||||||
...jest.requireActual('./handlers'),
|
...jest.requireActual('./handlers'),
|
||||||
getAgentHandler: jest.fn(),
|
getAgentHandler: jest.fn(),
|
||||||
|
@ -75,6 +77,10 @@ jest.mock('./handlers', () => ({
|
||||||
postRetrieveAgentsByActionsHandler: jest.fn(),
|
postRetrieveAgentsByActionsHandler: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('./migrate_handlers', () => ({
|
||||||
|
migrateSingleAgentHandler: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('./actions_handlers', () => ({
|
jest.mock('./actions_handlers', () => ({
|
||||||
postNewAgentActionHandlerBuilder: jest.fn(),
|
postNewAgentActionHandlerBuilder: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
@ -476,4 +482,20 @@ describe('schema validation', () => {
|
||||||
const validationResp = GetAvailableAgentVersionsResponseSchema.validate(expectedResponse);
|
const validationResp = GetAvailableAgentVersionsResponseSchema.validate(expectedResponse);
|
||||||
expect(validationResp).toEqual(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,
|
GetTagsRequestSchema,
|
||||||
GetOneAgentRequestSchema,
|
GetOneAgentRequestSchema,
|
||||||
UpdateAgentRequestSchema,
|
UpdateAgentRequestSchema,
|
||||||
|
MigrateSingleAgentRequestSchema,
|
||||||
DeleteAgentRequestSchema,
|
DeleteAgentRequestSchema,
|
||||||
PostAgentUnenrollRequestSchema,
|
PostAgentUnenrollRequestSchema,
|
||||||
PostBulkAgentUnenrollRequestSchema,
|
PostBulkAgentUnenrollRequestSchema,
|
||||||
|
@ -34,6 +35,7 @@ import {
|
||||||
PostRetrieveAgentsByActionsRequestSchema,
|
PostRetrieveAgentsByActionsRequestSchema,
|
||||||
DeleteAgentUploadFileRequestSchema,
|
DeleteAgentUploadFileRequestSchema,
|
||||||
GetTagsResponseSchema,
|
GetTagsResponseSchema,
|
||||||
|
MigrateSingleAgentResponseSchema,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import * as AgentService from '../../services/agents';
|
import * as AgentService from '../../services/agents';
|
||||||
import type { FleetConfigType } from '../..';
|
import type { FleetConfigType } from '../..';
|
||||||
|
@ -88,6 +90,7 @@ import {
|
||||||
bulkRequestDiagnosticsHandler,
|
bulkRequestDiagnosticsHandler,
|
||||||
requestDiagnosticsHandler,
|
requestDiagnosticsHandler,
|
||||||
} from './request_diagnostics_handler';
|
} from './request_diagnostics_handler';
|
||||||
|
import { migrateSingleAgentHandler } from './migrate_handlers';
|
||||||
|
|
||||||
export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => {
|
export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigType) => {
|
||||||
// Get one
|
// Get one
|
||||||
|
@ -123,6 +126,38 @@ export const registerAPIRoutes = (router: FleetAuthzRouter, config: FleetConfigT
|
||||||
getAgentHandler
|
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
|
// Update
|
||||||
router.versioned
|
router.versioned
|
||||||
.put({
|
.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,
|
traceparent: apm.currentTraceparent,
|
||||||
is_automatic: newAgentAction.is_automatic,
|
is_automatic: newAgentAction.is_automatic,
|
||||||
policyId: newAgentAction.policyId,
|
policyId: newAgentAction.policyId,
|
||||||
|
enrollment_token: newAgentAction.enrollment_token,
|
||||||
|
target_uri: newAgentAction.target_uri,
|
||||||
|
settings: newAgentAction.additionalSettings ? newAgentAction.additionalSettings : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const messageSigningService = appContextService.getMessageSigningService();
|
const messageSigningService = appContextService.getMessageSigningService();
|
||||||
|
@ -79,7 +82,6 @@ export async function createAgentAction(
|
||||||
signature: signedBody.signature,
|
signature: signedBody.signature,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
await esClient.create({
|
await esClient.create({
|
||||||
index: AGENT_ACTIONS_INDEX,
|
index: AGENT_ACTIONS_INDEX,
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
|
|
|
@ -15,6 +15,7 @@ export * from './reassign';
|
||||||
export * from './update_agent_tags';
|
export * from './update_agent_tags';
|
||||||
export * from './action_status';
|
export * from './action_status';
|
||||||
export * from './request_diagnostics';
|
export * from './request_diagnostics';
|
||||||
|
export * from './migrate';
|
||||||
export { getAgentUploads, getAgentUploadFile, deleteAgentUploadFile } from './uploads';
|
export { getAgentUploads, getAgentUploadFile, deleteAgentUploadFile } from './uploads';
|
||||||
export { AgentServiceImpl } from './agent_service';
|
export { AgentServiceImpl } from './agent_service';
|
||||||
export type { AgentClient, AgentService } 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([
|
export const AgentComponentStateSchema = schema.oneOf([
|
||||||
schema.literal('STARTING'),
|
schema.literal('STARTING'),
|
||||||
schema.literal('CONFIGURING'),
|
schema.literal('CONFIGURING'),
|
||||||
|
@ -523,6 +537,19 @@ export const UpdateAgentRequestSchema = {
|
||||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
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 = {
|
export const PostBulkUpdateAgentTagsRequestSchema = {
|
||||||
body: schema.object({
|
body: schema.object({
|
||||||
|
@ -657,6 +684,7 @@ export const GetActionStatusResponseSchema = schema.object({
|
||||||
schema.literal('UPDATE_TAGS'),
|
schema.literal('UPDATE_TAGS'),
|
||||||
schema.literal('POLICY_CHANGE'),
|
schema.literal('POLICY_CHANGE'),
|
||||||
schema.literal('INPUT_ACTION'),
|
schema.literal('INPUT_ACTION'),
|
||||||
|
schema.literal('MIGRATE'),
|
||||||
]),
|
]),
|
||||||
nbAgentsActioned: schema.number({
|
nbAgentsActioned: schema.number({
|
||||||
meta: {
|
meta: {
|
||||||
|
|
|
@ -27,5 +27,6 @@ export default function loadTests({ loadTestFile, getService }) {
|
||||||
loadTestFile(require.resolve('./uploads'));
|
loadTestFile(require.resolve('./uploads'));
|
||||||
loadTestFile(require.resolve('./get_agents_by_actions'));
|
loadTestFile(require.resolve('./get_agents_by_actions'));
|
||||||
loadTestFile(require.resolve('./privileges'));
|
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,
|
beforeEach: updateAgentBeforeEach,
|
||||||
afterEach: updateAgentAfterEach,
|
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',
|
method: 'DELETE',
|
||||||
path: '/api/fleet/agents/agent1',
|
path: '/api/fleet/agents/agent1',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue