[Fleet] Display outputs in agent list table and agent details (#195801)

Closes https://github.com/elastic/kibana/issues/192339

## Summary

Display two additional columns with Outputs hosts in agent list table
and agent details section
- The two columns show monitoring output and the integrations output and
link to the output flyout in settings
- Display a badge that show the outputs set per integration introduced
by https://github.com/elastic/kibana/pull/189125
- Same info displayed in agent details as well

To achieve this, I added two new endpoints.

1. Endpoint that fetches all the outputs associated with a single agent
policy (outputs defined on agent policy or default defined in global
settings and if any, outputs per integration)
```
GET kbn:/api/fleet/agent_policies/<AGENT_POLICY_ID>/outputs
```

2. Endpoint that fetches the outputs as above, for a defined set of
agent policy ids
```
POST kbn:/api/fleet/agent_policies/outputs
{
  "ids": ["policy_id1", "policy_id2", ...]
}
```
The reason to pass an array of ids is to ensure that we fetch the info
only for the policies displayed in the table at any given moment.


### Screenshots
**Agent list**
![Screenshot 2024-10-16 at 17 51
57](https://github.com/user-attachments/assets/3ee08df1-9562-497f-9621-4a913b3dad74)
![Screenshot 2024-10-16 at 17 52
05](https://github.com/user-attachments/assets/72b9da7d-872a-45f8-b02d-29184ffb2179)

**Agent details**
![Screenshot 2024-10-16 at 17 52
20](https://github.com/user-attachments/assets/b99aaf9e-14f1-44b8-9776-3e0136775af8)


### Checklist

- [ ] 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/packages/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

### For maintainers
- [ ] This will appear in the **Release Notes** and follow the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2024-10-22 14:46:24 +02:00 committed by GitHub
parent 3130492752
commit 3be33bd3e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 2548 additions and 43 deletions

View file

@ -9776,6 +9776,191 @@
]
}
},
"/api/fleet/agent_policies/outputs": {
"post": {
"description": "Get list of outputs associated with agent policies",
"operationId": "%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"ids": {
"description": "list of package policy ids",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"ids"
],
"type": "object"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"items": {
"items": {
"additionalProperties": false,
"properties": {
"agentPolicyId": {
"type": "string"
},
"data": {
"additionalProperties": false,
"properties": {
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"integrationPolicyName": {
"type": "string"
},
"name": {
"type": "string"
},
"pkgName": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
},
"monitoring": {
"additionalProperties": false,
"properties": {
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
}
},
"required": [
"monitoring",
"data"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"items"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
}
},
"summary": "",
"tags": [
"Elastic Agent policies"
]
}
},
"/api/fleet/agent_policies/{agentPolicyId}": {
"get": {
"description": "Get an agent policy by ID",
@ -12938,6 +13123,164 @@
]
}
},
"/api/fleet/agent_policies/{agentPolicyId}/outputs": {
"get": {
"description": "Get list of outputs associated with agent policy by policy id",
"operationId": "%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"in": "path",
"name": "agentPolicyId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"item": {
"additionalProperties": false,
"properties": {
"agentPolicyId": {
"type": "string"
},
"data": {
"additionalProperties": false,
"properties": {
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"integrationPolicyName": {
"type": "string"
},
"name": {
"type": "string"
},
"pkgName": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
},
"monitoring": {
"additionalProperties": false,
"properties": {
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
}
},
"required": [
"monitoring",
"data"
],
"type": "object"
}
},
"required": [
"item"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
}
},
"summary": "",
"tags": [
"Elastic Agent policies"
]
}
},
"/api/fleet/agent_status": {
"get": {
"description": "Get agent status summary",

View file

@ -9776,6 +9776,191 @@
]
}
},
"/api/fleet/agent_policies/outputs": {
"post": {
"description": "Get list of outputs associated with agent policies",
"operationId": "%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"description": "A required header to protect against CSRF attacks",
"in": "header",
"name": "kbn-xsrf",
"required": true,
"schema": {
"example": "true",
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"ids": {
"description": "list of package policy ids",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"ids"
],
"type": "object"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"items": {
"items": {
"additionalProperties": false,
"properties": {
"agentPolicyId": {
"type": "string"
},
"data": {
"additionalProperties": false,
"properties": {
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"integrationPolicyName": {
"type": "string"
},
"name": {
"type": "string"
},
"pkgName": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
},
"monitoring": {
"additionalProperties": false,
"properties": {
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
}
},
"required": [
"monitoring",
"data"
],
"type": "object"
},
"type": "array"
}
},
"required": [
"items"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
}
},
"summary": "",
"tags": [
"Elastic Agent policies"
]
}
},
"/api/fleet/agent_policies/{agentPolicyId}": {
"get": {
"description": "Get an agent policy by ID",
@ -12938,6 +13123,164 @@
]
}
},
"/api/fleet/agent_policies/{agentPolicyId}/outputs": {
"get": {
"description": "Get list of outputs associated with agent policy by policy id",
"operationId": "%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0",
"parameters": [
{
"description": "The version of the API to use",
"in": "header",
"name": "elastic-api-version",
"schema": {
"default": "2023-10-31",
"enum": [
"2023-10-31"
],
"type": "string"
}
},
{
"in": "path",
"name": "agentPolicyId",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"properties": {
"item": {
"additionalProperties": false,
"properties": {
"agentPolicyId": {
"type": "string"
},
"data": {
"additionalProperties": false,
"properties": {
"integrations": {
"items": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"integrationPolicyName": {
"type": "string"
},
"name": {
"type": "string"
},
"pkgName": {
"type": "string"
}
},
"type": "object"
},
"type": "array"
},
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
},
"monitoring": {
"additionalProperties": false,
"properties": {
"output": {
"additionalProperties": false,
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
],
"type": "object"
}
},
"required": [
"output"
],
"type": "object"
}
},
"required": [
"monitoring",
"data"
],
"type": "object"
}
},
"required": [
"item"
],
"type": "object"
}
}
}
},
"400": {
"content": {
"application/json; Elastic-Api-Version=2023-10-31": {
"schema": {
"additionalProperties": false,
"description": "Generic Error",
"properties": {
"error": {
"type": "string"
},
"message": {
"type": "string"
},
"statusCode": {
"type": "number"
}
},
"required": [
"message"
],
"type": "object"
}
}
}
}
},
"summary": "",
"tags": [
"Elastic Agent policies"
]
}
},
"/api/fleet/agent_status": {
"get": {
"description": "Get agent status summary",

View file

@ -14594,6 +14594,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/{agentPolicyId}/outputs:
get:
description: Get list of outputs associated with agent policy by policy id
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: agentPolicyId
required: true
schema:
type: string
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
item:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
required:
- item
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@ -14664,6 +14768,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/outputs:
post:
description: Get list of outputs associated with agent policies
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
ids:
description: list of package policy ids
items:
type: string
type: array
required:
- ids
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
items:
items:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
type: array
required:
- items
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary

View file

@ -14594,6 +14594,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/{agentPolicyId}/outputs:
get:
description: Get list of outputs associated with agent policy by policy id
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: agentPolicyId
required: true
schema:
type: string
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
item:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
required:
- item
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@ -14664,6 +14768,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/outputs:
post:
description: Get list of outputs associated with agent policies
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
ids:
description: list of package policy ids
items:
type: string
type: array
required:
- ids
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
items:
items:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
type: array
required:
- items
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary

View file

@ -18023,6 +18023,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/{agentPolicyId}/outputs:
get:
description: Get list of outputs associated with agent policy by policy id
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: agentPolicyId
required: true
schema:
type: string
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
item:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
required:
- item
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@ -18093,6 +18197,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/outputs:
post:
description: Get list of outputs associated with agent policies
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
ids:
description: list of package policy ids
items:
type: string
type: array
required:
- ids
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
items:
items:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
type: array
required:
- items
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary

View file

@ -18023,6 +18023,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/{agentPolicyId}/outputs:
get:
description: Get list of outputs associated with agent policy by policy id
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- in: path
name: agentPolicyId
required: true
schema:
type: string
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
item:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
required:
- item
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@ -18093,6 +18197,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_policies/outputs:
post:
description: Get list of outputs associated with agent policies
operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
parameters:
- description: The version of the API to use
in: header
name: elastic-api-version
schema:
default: '2023-10-31'
enum:
- '2023-10-31'
type: string
- description: A required header to protect against CSRF attacks
in: header
name: kbn-xsrf
required: true
schema:
example: 'true'
type: string
requestBody:
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
ids:
description: list of package policy ids
items:
type: string
type: array
required:
- ids
responses:
'200':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
type: object
properties:
items:
items:
additionalProperties: false
type: object
properties:
agentPolicyId:
type: string
data:
additionalProperties: false
type: object
properties:
integrations:
items:
additionalProperties: false
type: object
properties:
id:
type: string
integrationPolicyName:
type: string
name:
type: string
pkgName:
type: string
type: array
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
monitoring:
additionalProperties: false
type: object
properties:
output:
additionalProperties: false
type: object
properties:
id:
type: string
name:
type: string
required:
- id
- name
required:
- output
required:
- monitoring
- data
type: array
required:
- items
'400':
content:
application/json; Elastic-Api-Version=2023-10-31:
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
error:
type: string
message:
type: string
statusCode:
type: number
required:
- message
summary: ''
tags:
- Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary

View file

@ -81,6 +81,8 @@ export const AGENT_POLICY_API_ROUTES = {
DELETE_PATTERN: `${AGENT_POLICY_API_ROOT}/delete`,
FULL_INFO_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/full`,
FULL_INFO_DOWNLOAD_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/download`,
LIST_OUTPUTS_PATTERN: `${AGENT_POLICY_API_ROOT}/outputs`,
INFO_OUTPUTS_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/outputs`,
};
// Kubernetes Manifest API routes

View file

@ -197,6 +197,14 @@ export const agentPolicyRouteService = {
getResetAllPreconfiguredAgentPolicyPath: () => {
return PRECONFIGURATION_API_ROUTES.RESET_PATTERN;
},
getInfoOutputsPath: (agentPolicyId: string) => {
return AGENT_POLICY_API_ROUTES.INFO_OUTPUTS_PATTERN.replace('{agentPolicyId}', agentPolicyId);
},
getListOutputsPath: () => {
return AGENT_POLICY_API_ROUTES.LIST_OUTPUTS_PATTERN;
},
};
export const dataStreamRouteService = {

View file

@ -262,3 +262,24 @@ export interface AgentlessApiResponse {
id: string;
region_id: string;
}
// Definitions for agent policy outputs endpoints
export interface MinimalOutput {
name?: string;
id?: string;
}
export interface IntegrationsOutput extends MinimalOutput {
pkgName?: string;
integrationPolicyName?: string;
}
export interface OutputsForAgentPolicy {
agentPolicyId?: string;
monitoring: {
output: MinimalOutput;
};
data: {
output: MinimalOutput;
integrations?: IntegrationsOutput[];
};
}

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import type { AgentPolicy, NewAgentPolicy, FullAgentPolicy } from '../models';
import type {
AgentPolicy,
NewAgentPolicy,
FullAgentPolicy,
OutputsForAgentPolicy,
} from '../models';
import type { ListResult, ListWithKuery, BulkGetResult } from './common';
@ -93,3 +98,16 @@ export type FetchAllAgentPoliciesOptions = Pick<
export type FetchAllAgentPolicyIdsOptions = Pick<ListWithKuery, 'perPage' | 'kuery'> & {
spaceId?: string;
};
export interface GetAgentPolicyOutputsResponse {
item: OutputsForAgentPolicy;
}
export interface GetListAgentPolicyOutputsResponse {
items: OutputsForAgentPolicy[];
}
export interface GetListAgentPolicyOutputsRequest {
body: {
ids?: string[];
};
}

View file

@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import type { Agent, AgentPolicy } from '../../../../../types';
import { useAgentVersion } from '../../../../../hooks';
import { useAgentVersion, useGetInfoOutputsForPolicy } from '../../../../../hooks';
import { ExperimentalFeaturesService, isAgentUpgradeable } from '../../../../../services';
import { AgentPolicySummaryLine } from '../../../../../components';
import { AgentHealth } from '../../../components';
@ -30,6 +30,7 @@ import { Tags } from '../../../components/tags';
import { formatAgentCPU, formatAgentMemory } from '../../../services/agent_metrics';
import { AgentDashboardLink } from '../agent_dashboard_link';
import { AgentUpgradeStatus } from '../../../agent_list_page/components/agent_upgrade_status';
import { AgentPolicyOutputsSummary } from '../../../agent_list_page/components/agent_policy_outputs_summary';
// Allows child text to be truncated
const FlexItemWithMinWidth = styled(EuiFlexItem)`
@ -43,10 +44,17 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
const latestAgentVersion = useAgentVersion();
const { displayAgentMetrics } = ExperimentalFeaturesService.get();
const outputRes = useGetInfoOutputsForPolicy(agentPolicy?.id);
const outputs = outputRes?.data?.item;
return (
<EuiPanel>
<EuiDescriptionList compressed>
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexGroup
direction="column"
gutterSize="m"
data-test-subj="agentDetailsOverviewSection"
>
{displayAgentMetrics && (
<EuiFlexGroup>
<FlexItemWithMinWidth grow={5}>
@ -206,6 +214,22 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
? agent.local_metadata.host.id
: '-',
},
{
title: i18n.translate('xpack.fleet.agentDetails.outputForMonitoringLabel', {
defaultMessage: 'Output for integrations',
}),
description: outputs ? <AgentPolicyOutputsSummary outputs={outputs} /> : '-',
},
{
title: i18n.translate('xpack.fleet.agentDetails.outputForMonitoringLabel', {
defaultMessage: 'Output for monitoring',
}),
description: outputs ? (
<AgentPolicyOutputsSummary outputs={outputs} isMonitoring={true} />
) : (
'-'
),
},
{
title: i18n.translate('xpack.fleet.agentDetails.logLevel', {
defaultMessage: 'Logging level',

View file

@ -4,7 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import React, { useCallback, useMemo } from 'react';
import type { EuiBasicTableColumn } from '@elastic/eui';
import { type CriteriaWithPagination } from '@elastic/eui';
import {
EuiBasicTable,
@ -23,20 +24,31 @@ import { isAgentUpgradeable, ExperimentalFeaturesService } from '../../../../ser
import { AgentHealth } from '../../components';
import type { Pagination } from '../../../../hooks';
import { useAgentVersion } from '../../../../hooks';
import { useAgentVersion, useGetListOutputsForPolicies } from '../../../../hooks';
import { useLink, useAuthz } from '../../../../hooks';
import { AgentPolicySummaryLine } from '../../../../components';
import { Tags } from '../../components/tags';
import type { AgentMetrics } from '../../../../../../../common/types';
import type { AgentMetrics, OutputsForAgentPolicy } from '../../../../../../../common/types';
import { formatAgentCPU, formatAgentMemory } from '../../services/agent_metrics';
import { AgentPolicyOutputsSummary } from './agent_policy_outputs_summary';
import { AgentUpgradeStatus } from './agent_upgrade_status';
import { EmptyPrompt } from './empty_prompt';
const VERSION_FIELD = 'local_metadata.elastic.agent.version';
const HOSTNAME_FIELD = 'local_metadata.host.hostname';
const AGENTS_TABLE_FIELDS = {
ACTIVE: 'active',
HOSTNAME: 'local_metadata.host.hostname',
POLICY: 'policy_id',
METRICS: 'metrics',
VERSION: 'local_metadata.elastic.agent.version',
LAST_CHECKIN: 'last_checkin',
OUTPUT_INTEGRATION: 'output_integrations',
OUTPUT_MONITORING: 'output_monitoring',
};
function safeMetadata(val: any) {
if (typeof val !== 'string') {
return '-';
@ -96,14 +108,33 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
const { getHref } = useLink();
const latestAgentVersion = useAgentVersion();
const isAgentSelectable = (agent: Agent) => {
if (!agent.active) return false;
if (!agent.policy_id) return true;
const isAgentSelectable = useCallback(
(agent: Agent) => {
if (!agent.active) return false;
if (!agent.policy_id) return true;
const agentPolicy = agentPoliciesIndexedById[agent.policy_id];
const isHosted = agentPolicy?.is_managed === true;
return !isHosted;
};
const agentPolicy = agentPoliciesIndexedById[agent.policy_id];
const isHosted = agentPolicy?.is_managed === true;
return !isHosted;
},
[agentPoliciesIndexedById]
);
const agentsShown = useMemo(() => {
return totalAgents
? showUpgradeable
? agents.filter((agent) => isAgentSelectable(agent) && isAgentUpgradeable(agent))
: agents
: [];
}, [agents, isAgentSelectable, showUpgradeable, totalAgents]);
// get the policyIds of the agents shown on the page
const policyIds = useMemo(() => {
return agentsShown.map((agent) => agent?.policy_id ?? '');
}, [agentsShown]);
const allOutputs = useGetListOutputsForPolicies({
ids: policyIds,
});
const noItemsMessage =
isLoading && isCurrentRequestIncremented ? (
@ -140,9 +171,9 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
},
};
const columns = [
const columns: Array<EuiBasicTableColumn<Agent>> = [
{
field: 'active',
field: AGENTS_TABLE_FIELDS.ACTIVE,
sortable: false,
width: '85px',
name: i18n.translate('xpack.fleet.agentList.statusColumnTitle', {
@ -151,7 +182,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
render: (active: boolean, agent: any) => <AgentHealth agent={agent} />,
},
{
field: HOSTNAME_FIELD,
field: AGENTS_TABLE_FIELDS.HOSTNAME,
sortable: true,
name: i18n.translate('xpack.fleet.agentList.hostColumnTitle', {
defaultMessage: 'Host',
@ -171,7 +202,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
),
},
{
field: 'policy_id',
field: AGENTS_TABLE_FIELDS.POLICY,
sortable: true,
truncateText: true,
name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', {
@ -208,7 +239,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
...(displayAgentMetrics
? [
{
field: 'metrics',
field: AGENTS_TABLE_FIELDS.METRICS,
sortable: false,
name: (
<EuiToolTip
@ -234,7 +265,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
),
},
{
field: 'metrics',
field: AGENTS_TABLE_FIELDS.METRICS,
sortable: false,
name: (
<EuiToolTip
@ -264,19 +295,52 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
},
]
: []),
{
field: 'last_checkin',
field: AGENTS_TABLE_FIELDS.LAST_CHECKIN,
sortable: true,
name: i18n.translate('xpack.fleet.agentList.lastCheckinTitle', {
defaultMessage: 'Last activity',
}),
width: '180px',
render: (lastCheckin: string, agent: any) =>
lastCheckin ? <FormattedRelative value={lastCheckin} /> : null,
width: '100px',
render: (lastCheckin: string) =>
lastCheckin ? <FormattedRelative value={lastCheckin} /> : undefined,
},
{
field: VERSION_FIELD,
field: AGENTS_TABLE_FIELDS.OUTPUT_INTEGRATION,
sortable: true,
truncateText: true,
name: i18n.translate('xpack.fleet.agentList.integrationsOutputTitle', {
defaultMessage: 'Output for integrations',
}),
width: '180px',
render: (outputs: OutputsForAgentPolicy[], agent: Agent) => {
if (!agent?.policy_id) return null;
const outputsForPolicy = allOutputs?.data?.items.find(
(item) => item.agentPolicyId === agent?.policy_id
);
return <AgentPolicyOutputsSummary outputs={outputsForPolicy} />;
},
},
{
field: AGENTS_TABLE_FIELDS.OUTPUT_MONITORING,
sortable: true,
truncateText: true,
name: i18n.translate('xpack.fleet.agentList.monitoringOutputTitle', {
defaultMessage: 'Output for monitoring',
}),
width: '180px',
render: (outputs: OutputsForAgentPolicy[], agent: Agent) => {
if (!agent?.policy_id) return null;
const outputsForPolicy = allOutputs?.data?.items.find(
(item) => item.agentPolicyId === agent?.policy_id
);
return <AgentPolicyOutputsSummary outputs={outputsForPolicy} isMonitoring={true} />;
},
},
{
field: AGENTS_TABLE_FIELDS.VERSION,
sortable: true,
width: '220px',
name: i18n.translate('xpack.fleet.agentList.versionTitle', {
@ -322,13 +386,7 @@ export const AgentListTable: React.FC<Props> = (props: Props) => {
data-test-subj="fleetAgentListTable"
loading={isLoading}
noItemsMessage={noItemsMessage}
items={
totalAgents
? showUpgradeable
? agents.filter((agent) => isAgentSelectable(agent) && isAgentUpgradeable(agent))
: agents
: []
}
items={agentsShown}
itemId="id"
columns={columns}
pagination={{

View file

@ -0,0 +1,99 @@
/*
* 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 { act, fireEvent } from '@testing-library/react';
import React from 'react';
import { createFleetTestRendererMock } from '../../../../../../mock';
import type { TestRenderer } from '../../../../../../mock';
import type { OutputsForAgentPolicy } from '../../../../../../../common/types';
import { AgentPolicyOutputsSummary } from './agent_policy_outputs_summary';
describe('MultipleAgentPolicySummaryLine', () => {
let testRenderer: TestRenderer;
const outputsForPolicy: OutputsForAgentPolicy = {
agentPolicyId: 'policy-1',
monitoring: {
output: {
id: 'elasticsearch1',
name: 'Elasticsearch1',
},
},
data: {
output: {
id: 'elasticsearch1',
name: 'Elasticsearch1',
},
},
};
const data = {
data: {
output: {
id: 'elasticsearch1',
name: 'Elasticsearch1',
},
integrations: [
{
id: 'remote_es1',
name: 'Remote ES',
pkgName: 'ngnix',
integrationPolicyName: 'Nginx-1',
},
{
id: 'logstash',
name: 'Logstash-1',
pkgName: 'apache',
integrationPolicyName: 'Apache-1',
},
],
},
};
const render = (outputs?: OutputsForAgentPolicy, isMonitoring?: boolean) =>
testRenderer.render(
<AgentPolicyOutputsSummary outputs={outputs} isMonitoring={isMonitoring} />
);
beforeEach(() => {
testRenderer = createFleetTestRendererMock();
});
test('it should render the name associated with the default output when the agent policy does not have custom outputs', async () => {
const results = render(outputsForPolicy);
expect(results.container.textContent).toBe('Elasticsearch1');
expect(results.queryByTestId('outputNameLink')).toBeInTheDocument();
expect(results.queryByTestId('outputsIntegrationsNumberBadge')).not.toBeInTheDocument();
});
test('it should render the first output name and the badge when there are multiple outputs associated with integrations', async () => {
const results = render({ ...outputsForPolicy, ...data });
expect(results.queryByTestId('outputNameLink')).toBeInTheDocument();
expect(results.queryByTestId('outputsIntegrationsNumberBadge')).toBeInTheDocument();
await act(async () => {
fireEvent.click(results.getByTestId('outputsIntegrationsNumberBadge'));
});
expect(results.queryByTestId('outputPopover')).toBeInTheDocument();
expect(results.queryByTestId('output-integration-0')?.textContent).toContain(
'Nginx-1: Remote ES'
);
expect(results.queryByTestId('output-integration-1')?.textContent).toContain(
'Apache-1: Logstash-1'
);
});
test('it should not render the badge when monitoring is true', async () => {
const results = render({ ...outputsForPolicy, ...data }, true);
expect(results.queryByTestId('outputNameLink')).toBeInTheDocument();
expect(results.queryByTestId('outputsIntegrationsNumberBadge')).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,115 @@
/*
* 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 styled from 'styled-components';
import React, { useMemo, useState } from 'react';
import type { EuiListGroupItemProps } from '@elastic/eui';
import {
EuiBadge,
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiListGroup,
EuiPopover,
EuiPopoverTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useLink } from '../../../../hooks';
import type { OutputsForAgentPolicy } from '../../../../../../../common/types';
const TruncatedEuiLink = styled(EuiLink)`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 120px;
`;
export const AgentPolicyOutputsSummary: React.FC<{
outputs?: OutputsForAgentPolicy;
isMonitoring?: boolean;
}> = ({ outputs, isMonitoring }) => {
const { getHref } = useLink();
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const closePopover = () => setIsPopoverOpen(false);
const monitoring = outputs?.monitoring;
const data = outputs?.data;
const listItems: EuiListGroupItemProps[] = useMemo(() => {
if (!data?.integrations) return [];
return (data?.integrations || []).map((integration, index) => {
return {
'data-test-subj': `output-integration-${index}`,
label: `${integration.integrationPolicyName}: ${integration.name}`,
href: getHref('settings_edit_outputs', { outputId: integration?.id ?? '' }),
iconType: 'dot',
};
});
}, [getHref, data?.integrations]);
return (
<EuiFlexGroup
gutterSize="s"
alignItems="baseline"
responsive={false}
justifyContent="flexStart"
>
{isMonitoring ? (
<EuiFlexItem grow={false}>
<TruncatedEuiLink
href={getHref('settings_edit_outputs', { outputId: monitoring?.output?.id ?? '' })}
title={monitoring?.output.name}
data-test-subj="outputNameLink"
>
{monitoring?.output.name}
</TruncatedEuiLink>
</EuiFlexItem>
) : (
<EuiFlexItem grow={false}>
<TruncatedEuiLink
href={getHref('settings_edit_outputs', { outputId: data?.output?.id ?? '' })}
title={data?.output.name}
data-test-subj="outputNameLink"
>
{data?.output.name}
</TruncatedEuiLink>
</EuiFlexItem>
)}
{data?.integrations && data?.integrations.length >= 1 && !isMonitoring && (
<EuiFlexItem grow={false}>
<EuiBadge
color="hollow"
data-test-subj="outputsIntegrationsNumberBadge"
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
onClickAriaLabel="Open output integrations popover"
>
+{data?.integrations.length}
</EuiBadge>
<EuiPopover
data-test-subj="outputPopover"
isOpen={isPopoverOpen}
closePopover={closePopover}
anchorPosition="downCenter"
>
<EuiPopoverTitle>
{i18n.translate('xpack.fleet.AgentPolicyOutputsSummary.popover.title', {
defaultMessage: 'Output for integrations',
})}
</EuiPopoverTitle>
<div style={{ width: '280px' }}>
<EuiListGroup listItems={listItems} color="primary" size="s" gutterSize="none" />
</div>
</EuiPopover>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
};

View file

@ -23,6 +23,9 @@ import type {
DeleteAgentPolicyRequest,
DeleteAgentPolicyResponse,
BulkGetAgentPoliciesResponse,
GetAgentPolicyOutputsResponse,
GetListAgentPolicyOutputsResponse,
GetListAgentPolicyOutputsRequest,
} from '../../types';
import { useRequest, sendRequest, useConditionalRequest, sendRequestForRq } from './use_request';
@ -201,3 +204,21 @@ export const sendResetAllPreconfiguredAgentPolicies = () => {
version: API_VERSIONS.internal.v1,
});
};
export const useGetListOutputsForPolicies = (body?: GetListAgentPolicyOutputsRequest['body']) => {
return useRequest<GetListAgentPolicyOutputsResponse>({
path: agentPolicyRouteService.getListOutputsPath(),
method: 'post',
body: JSON.stringify(body),
version: API_VERSIONS.public.v1,
});
};
export const useGetInfoOutputsForPolicy = (agentPolicyId: string | undefined) => {
return useConditionalRequest<GetAgentPolicyOutputsResponse>({
path: agentPolicyId ? agentPolicyRouteService.getInfoOutputsPath(agentPolicyId) : undefined,
method: 'get',
shouldSendRequest: !!agentPolicyId,
version: API_VERSIONS.public.v1,
} as SendConditionalRequestConfig);
};

View file

@ -147,6 +147,9 @@ export type {
GetEnrollmentSettingsRequest,
GetEnrollmentSettingsResponse,
GetSpaceSettingsResponse,
GetAgentPolicyOutputsResponse,
GetListAgentPolicyOutputsRequest,
GetListAgentPolicyOutputsResponse,
} from '../../common/types';
export {
entries,

View file

@ -33,6 +33,8 @@ import type {
BulkGetAgentPoliciesRequestSchema,
AgentPolicy,
FleetRequestHandlerContext,
GetAgentPolicyOutputsRequestSchema,
GetListAgentPolicyOutputsRequestSchema,
} from '../../types';
import type {
@ -47,6 +49,8 @@ import type {
GetFullAgentConfigMapResponse,
GetFullAgentManifestResponse,
BulkGetAgentPoliciesResponse,
GetAgentPolicyOutputsResponse,
GetListAgentPolicyOutputsResponse,
} from '../../../common/types';
import {
defaultFleetErrorHandler,
@ -678,3 +682,64 @@ export const downloadK8sManifest: FleetRequestHandler<
return defaultFleetErrorHandler({ error, response });
}
};
export const GetAgentPolicyOutputsHandler: FleetRequestHandler<
TypeOf<typeof GetAgentPolicyOutputsRequestSchema.params>,
undefined
> = async (context, request, response) => {
try {
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
const agentPolicy = await agentPolicyService.get(soClient, request.params.agentPolicyId);
if (!agentPolicy) {
return response.customError({
statusCode: 404,
body: { message: 'Agent policy not found' },
});
}
const outputs = await agentPolicyService.getAllOutputsForPolicy(soClient, agentPolicy);
const body: GetAgentPolicyOutputsResponse = {
item: outputs,
};
return response.ok({
body,
});
} catch (error) {
return defaultFleetErrorHandler({ error, response });
}
};
export const GetListAgentPolicyOutputsHandler: FleetRequestHandler<
undefined,
undefined,
TypeOf<typeof GetListAgentPolicyOutputsRequestSchema.body>
> = async (context, request, response) => {
try {
const coreContext = await context.core;
const soClient = coreContext.savedObjects.client;
const { ids } = request.body;
if (!ids) {
return response.ok({
body: { items: [] },
});
}
const agentPolicies = await agentPolicyService.getByIDs(soClient, ids, {
withPackagePolicies: true,
});
const outputsList = await agentPolicyService.listAllOutputsForPolicies(soClient, agentPolicies);
const body: GetListAgentPolicyOutputsResponse = {
items: outputsList,
};
return response.ok({
body,
});
} catch (error) {
return defaultFleetErrorHandler({ error, response });
}
};

View file

@ -28,6 +28,10 @@ import {
GetFullAgentPolicyResponseSchema,
DownloadFullAgentPolicyResponseSchema,
GetK8sManifestResponseScheme,
GetAgentPolicyOutputsRequestSchema,
GetAgentPolicyOutputsResponseSchema,
GetListAgentPolicyOutputsResponseSchema,
GetListAgentPolicyOutputsRequestSchema,
} from '../../types';
import { K8S_API_ROUTES } from '../../../common/constants';
@ -47,6 +51,8 @@ import {
downloadK8sManifest,
getK8sManifest,
bulkGetAgentPoliciesHandler,
GetAgentPolicyOutputsHandler,
GetListAgentPolicyOutputsHandler,
} from './handlers';
export const registerRoutes = (router: FleetAuthzRouter) => {
@ -390,4 +396,62 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
},
downloadK8sManifest
);
router.versioned
.post({
path: AGENT_POLICY_API_ROUTES.LIST_OUTPUTS_PATTERN,
fleetAuthz: (authz) => {
return authz.fleet.readAgentPolicies && authz.fleet.readSettings;
},
description: `Get list of outputs associated with agent policies`,
options: {
tags: ['oas-tag:Elastic Agent policies'],
},
})
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: GetListAgentPolicyOutputsRequestSchema,
response: {
200: {
body: () => GetListAgentPolicyOutputsResponseSchema,
},
400: {
body: genericErrorResponse,
},
},
},
},
GetListAgentPolicyOutputsHandler
);
router.versioned
.get({
path: AGENT_POLICY_API_ROUTES.INFO_OUTPUTS_PATTERN,
fleetAuthz: (authz) => {
return authz.fleet.readAgentPolicies && authz.fleet.readSettings;
},
description: `Get list of outputs associated with agent policy by policy id`,
options: {
tags: ['oas-tag:Elastic Agent policies'],
},
})
.addVersion(
{
version: API_VERSIONS.public.v1,
validate: {
request: GetAgentPolicyOutputsRequestSchema,
response: {
200: {
body: () => GetAgentPolicyOutputsResponseSchema,
},
400: {
body: genericErrorResponse,
},
},
},
},
GetAgentPolicyOutputsHandler
);
};

View file

@ -109,7 +109,7 @@ jest.mock('../output', () => {
getDefaultDataOutputId: async () => 'test-id',
getDefaultMonitoringOutputId: async () => 'test-id',
get: (soClient: any, id: string): Output => OUTPUTS[id] || OUTPUTS['test-id'],
bulkGet: async (soClient: any, ids: string[]): Promise<Output[]> => {
bulkGet: async (ids: string[]): Promise<Output[]> => {
return ids.map((id) => OUTPUTS[id] || OUTPUTS['test-id']);
},
},

View file

@ -48,7 +48,7 @@ export async function fetchRelatedSavedObjects(
const [outputs, { host: downloadSourceUri, proxy_id: downloadSourceProxyId }, fleetServerHosts] =
await Promise.all([
outputService.bulkGet(soClient, outputIds, { ignoreNotFound: true }),
outputService.bulkGet(outputIds, { ignoreNotFound: true }),
getSourceUriForAgentPolicy(soClient, agentPolicy),
getFleetServerHostsForAgentPolicy(soClient, agentPolicy).catch((err) => {
appContextService

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { chunk, groupBy, isEqual, keyBy, omit, pick } from 'lodash';
import { chunk, groupBy, isEqual, keyBy, omit, pick, uniq } from 'lodash';
import { v5 as uuidv5 } from 'uuid';
import { dump } from 'js-yaml';
import pMap from 'p-map';
@ -61,6 +61,7 @@ import type {
PostAgentPolicyCreateCallback,
PostAgentPolicyUpdateCallback,
PreconfiguredAgentPolicy,
OutputsForAgentPolicy,
} from '../types';
import {
AGENT_POLICY_INDEX,
@ -74,6 +75,7 @@ import type {
FetchAllAgentPoliciesOptions,
FetchAllAgentPolicyIdsOptions,
FleetServerPolicy,
IntegrationsOutput,
PackageInfo,
} from '../../common/types';
import {
@ -85,6 +87,7 @@ import {
HostedAgentPolicyRestrictionRelatedError,
PackagePolicyRestrictionRelatedError,
AgentlessPolicyExistsRequestError,
OutputNotFoundError,
} from '../errors';
import type { FullAgentConfigMap } from '../../common/types/models/agent_cm';
@ -1777,6 +1780,95 @@ class AgentPolicyService {
});
}
// Get all the outputs per agent policy
public async getAllOutputsForPolicy(
soClient: SavedObjectsClientContract,
agentPolicy: AgentPolicy
) {
const logger = appContextService.getLogger();
const [defaultDataOutputId, defaultMonitoringOutputId] = await Promise.all([
outputService.getDefaultDataOutputId(soClient),
outputService.getDefaultMonitoringOutputId(soClient),
]);
if (!defaultDataOutputId) {
throw new OutputNotFoundError('Default output is not setup');
}
const dataOutputId = agentPolicy.data_output_id || defaultDataOutputId;
const monitoringOutputId =
agentPolicy.monitoring_output_id || defaultMonitoringOutputId || dataOutputId;
const outputIds = uniq([dataOutputId, monitoringOutputId]);
const fetchedOutputs = await outputService.bulkGet(outputIds, {
ignoreNotFound: true,
});
const dataOutput = fetchedOutputs.find((output) => output.id === dataOutputId);
const monitoringOutput = fetchedOutputs.find((output) => output.id === monitoringOutputId);
let integrationsDataOutputs: IntegrationsOutput[] = [];
if (agentPolicy?.package_policies) {
const integrationsWithOutputs = agentPolicy.package_policies.filter(
(pkgPolicy) => !!pkgPolicy?.output_id
);
integrationsDataOutputs = await pMap(
integrationsWithOutputs,
async (pkgPolicy) => {
if (pkgPolicy?.output_id) {
try {
const output = await outputService.get(soClient, pkgPolicy.output_id);
return { integrationPolicyName: pkgPolicy?.name, id: output.id, name: output.name };
} catch (error) {
logger.error(
`error while retrieving output with id "${pkgPolicy.output_id}": ${error}`
);
}
}
return { integrationPolicyName: pkgPolicy?.name, id: pkgPolicy?.output_id ?? '' };
},
{
concurrency: 20,
}
);
}
const outputs: OutputsForAgentPolicy = {
monitoring: {
output: {
name: monitoringOutput?.name ?? '',
id: monitoringOutput?.id ?? '',
},
},
data: {
output: {
name: dataOutput?.name ?? '',
id: dataOutput?.id ?? '',
},
integrations: integrationsDataOutputs ?? [],
},
};
return outputs;
}
public async listAllOutputsForPolicies(
soClient: SavedObjectsClientContract,
agentPolicies: AgentPolicy[]
) {
const allOutputs: OutputsForAgentPolicy[] = await pMap(
agentPolicies,
async (agentPolicy) => {
const output = await this.getAllOutputsForPolicy(soClient, agentPolicy);
return { agentPolicyId: agentPolicy.id, ...output };
},
{
concurrency: 50,
}
);
return allOutputs;
}
private checkTamperProtectionLicense(agentPolicy: { is_protected?: boolean }): void {
if (agentPolicy?.is_protected && !licenseService.isPlatinum()) {
throw new FleetUnauthorizedError('Tamper protection requires Platinum license');

View file

@ -705,11 +705,7 @@ class OutputService {
return outputSavedObjectToOutput(newSo);
}
public async bulkGet(
soClient: SavedObjectsClientContract,
ids: string[],
{ ignoreNotFound = false } = { ignoreNotFound: true }
) {
public async bulkGet(ids: string[], { ignoreNotFound = false } = { ignoreNotFound: true }) {
const res = await this.encryptedSoClient.bulkGet<OutputSOAttributes>(
ids.map((id) => ({ id: outputIdToUuid(id), type: SAVED_OBJECT_TYPE }))
);

View file

@ -74,7 +74,6 @@ export async function createOrUpdatePreconfiguredOutputs(
}
const existingOutputs = await outputService.bulkGet(
soClient,
outputs.map(({ id }) => id),
{ ignoreNotFound: true }
);

View file

@ -101,6 +101,7 @@ export type {
InstallLatestExecutedState,
TemplateAgentPolicyInput,
NewPackagePolicyInput,
OutputsForAgentPolicy,
} from '../../common/types';
export { ElasticsearchAssetType, KibanaAssetType, KibanaSavedObjectType } from '../../common/types';
export { dataTypes } from '../../common/constants';

View file

@ -393,3 +393,35 @@ export const FullAgentPolicyResponseSchema = schema.object({
})
),
});
const MinimalOutputSchema = schema.object({
id: schema.string(),
name: schema.string(),
});
const IntegrationsOutputSchema = schema.arrayOf(
schema.object({
pkgName: schema.maybe(schema.string()),
integrationPolicyName: schema.maybe(schema.string()),
id: schema.maybe(schema.string()),
name: schema.maybe(schema.string()),
})
);
const OutputsForAgentPolicySchema = schema.object({
agentPolicyId: schema.maybe(schema.string()),
monitoring: schema.object({
output: MinimalOutputSchema,
}),
data: schema.object({
output: MinimalOutputSchema,
integrations: schema.maybe(IntegrationsOutputSchema),
}),
});
export const GetAgentPolicyOutputsResponseSchema = schema.object({
item: OutputsForAgentPolicySchema,
});
export const GetListAgentPolicyOutputsResponseSchema = schema.object({
items: schema.arrayOf(OutputsForAgentPolicySchema),
});

View file

@ -171,3 +171,17 @@ export const GetK8sManifestRequestSchema = {
export const GetK8sManifestResponseScheme = schema.object({
item: schema.string(),
});
export const GetAgentPolicyOutputsRequestSchema = {
params: schema.object({
agentPolicyId: schema.string(),
}),
};
export const GetListAgentPolicyOutputsRequestSchema = {
body: schema.object({
ids: schema.arrayOf(schema.string(), {
meta: { description: 'list of package policy ids' },
}),
}),
};

View file

@ -0,0 +1,282 @@
/*
* 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 expect from '@kbn/expect';
import { CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const supertest = getService('supertest');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const fleetAndAgents = getService('fleetAndAgents');
const createOutput = async ({
name,
id,
type,
hosts,
}: {
name: string;
id: string;
type: string;
hosts: string[];
}): Promise<string> => {
const res = await supertest
.post(`/api/fleet/outputs`)
.set('kbn-xsrf', 'xxxx')
.send({
id,
name,
type,
hosts,
})
.expect(200);
return res.body.item.id;
};
const createAgentPolicy = async (
name: string,
id: string,
dataOutputId?: string,
monitoringOutputId?: string
): Promise<CreateAgentPolicyResponse> => {
const res = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
name,
id,
namespace: 'default',
...(dataOutputId ? { data_output_id: dataOutputId } : {}),
...(monitoringOutputId ? { monitoring_output_id: monitoringOutputId } : {}),
})
.expect(200);
return res.body.item;
};
const createAgentPolicyWithPackagePolicy = async ({
name,
id,
outputId,
}: {
name: string;
id: string;
outputId?: string;
}): Promise<CreateAgentPolicyResponse> => {
const { body: res } = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
name,
namespace: 'default',
id,
})
.expect(200);
const agentPolicyWithPPId = res.item.id;
// package policy needs to have a custom output_id
await supertest
.post(`/api/fleet/package_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'filetest-1',
description: '',
namespace: 'default',
...(outputId ? { output_id: outputId } : {}),
policy_id: agentPolicyWithPPId,
inputs: [],
package: {
name: 'filetest',
title: 'For File Tests',
version: '0.1.0',
},
})
.expect(200);
return res.item;
};
let output1Id = '';
describe('fleet_agent_policies_outputs', () => {
describe('POST /api/fleet/agent_policies/outputs', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
await kibanaServer.savedObjects.cleanStandardList();
await fleetAndAgents.setup();
output1Id = await createOutput({
name: 'Output 1',
id: 'logstash-output-1',
type: 'logstash',
hosts: ['test.fr:443'],
});
});
after(async () => {
await supertest
.delete(`/api/fleet/outputs/${output1Id}`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
});
it('should get a list of outputs by agent policies', async () => {
await createAgentPolicy('Agent policy with default output', 'agent-policy-1');
await createAgentPolicy(
'Agent policy with custom output',
'agent-policy-2',
output1Id,
output1Id
);
const outputsPerPoliciesRes = await supertest
.post(`/api/fleet/agent_policies/outputs`)
.set('kbn-xsrf', 'xxxx')
.send({
ids: ['agent-policy-1', 'agent-policy-2'],
})
.expect(200);
expect(outputsPerPoliciesRes.body.items).to.eql([
{
agentPolicyId: 'agent-policy-1',
monitoring: {
output: {
name: 'default',
id: 'fleet-default-output',
},
},
data: {
output: {
name: 'default',
id: 'fleet-default-output',
},
integrations: [],
},
},
{
agentPolicyId: 'agent-policy-2',
monitoring: {
output: {
name: 'Output 1',
id: 'logstash-output-1',
},
},
data: {
output: {
name: 'Output 1',
id: 'logstash-output-1',
},
integrations: [],
},
},
]);
// clean up policies
await supertest
.post(`/api/fleet/agent_policies/delete`)
.send({ agentPolicyId: 'agent-policy-1' })
.set('kbn-xsrf', 'xxxx')
.expect(200);
await supertest
.post(`/api/fleet/agent_policies/delete`)
.send({ agentPolicyId: 'agent-policy-2' })
.set('kbn-xsrf', 'xxxx')
.expect(200);
});
});
let output2Id = '';
describe('GET /api/fleet/agent_policies/{agentPolicyId}/outputs', () => {
before(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
await kibanaServer.savedObjects.cleanStandardList();
await fleetAndAgents.setup();
output2Id = await createOutput({
name: 'ES Output 1',
id: 'es-output-1',
type: 'elasticsearch',
hosts: ['https://test.fr:8080'],
});
});
after(async () => {
await supertest
.delete(`/api/fleet/outputs/${output2Id}`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
});
it('should get the list of outputs related to an agentPolicy id', async () => {
await createAgentPolicy('Agent policy with ES output', 'agent-policy-custom', output2Id);
const outputsPerPoliciesRes = await supertest
.get(`/api/fleet/agent_policies/agent-policy-custom/outputs`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
expect(outputsPerPoliciesRes.body.item).to.eql({
monitoring: {
output: {
name: 'default',
id: 'fleet-default-output',
},
},
data: {
output: {
name: 'ES Output 1',
id: 'es-output-1',
},
integrations: [],
},
});
await supertest
.post(`/api/fleet/agent_policies/delete`)
.send({ agentPolicyId: 'agent-policy-custom' })
.set('kbn-xsrf', 'xxxx')
.expect(200);
});
it('should also list the outputs set on integrations if any', async () => {
await createAgentPolicyWithPackagePolicy({
name: 'Agent Policy with package policy',
id: 'agent-policy-custom-2',
outputId: output2Id,
});
const outputsPerPoliciesRes = await supertest
.get(`/api/fleet/agent_policies/agent-policy-custom-2/outputs`)
.set('kbn-xsrf', 'xxxx')
.expect(200);
expect(outputsPerPoliciesRes.body.item).to.eql({
monitoring: {
output: {
name: 'default',
id: 'fleet-default-output',
},
},
data: {
output: {
name: 'default',
id: 'fleet-default-output',
},
integrations: [
{
id: 'es-output-1',
integrationPolicyName: 'filetest-1',
name: 'ES Output 1',
},
],
},
});
await supertest
.post(`/api/fleet/agent_policies/delete`)
.send({ agentPolicyId: 'agent-policy-custom-2' })
.set('kbn-xsrf', 'xxxx')
.expect(200);
});
});
});
}

View file

@ -13,5 +13,6 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./privileges'));
loadTestFile(require.resolve('./agent_policy_root_integrations'));
loadTestFile(require.resolve('./create_standalone_api_key'));
loadTestFile(require.resolve('./agent_policy_outputs'));
});
}