From e8b9116916ee61478d2d288adf1f719c23af9100 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Tue, 26 Nov 2024 00:12:14 -0800 Subject: [PATCH] [UII] Add status tracking for agentless integrations (#199567) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Resolves https://github.com/elastic/ingest-dev/issues/3933. For deployments that support agentless, integrations with agentless deployment mode enabled will allow the status of agentless integration policies to be tracked. ### Key technical changes - A new field `supports_agentless` was added to package policies. This field already exists on agent policies. When an agentless integration is created, `supports_agentless: true` is now added to both the package policy and its parent agent policy. - This allows easier filtering for agentless integrations as we avoid having to retrieve & check against every parent agent policy. - This also means existing agentless policies do not get this new status tracking UI, only new ones created after this change. Since agentless is not yet GA, I think this is okay. - `/api/fleet/agent_status/data` now takes optional query params `pkgName` and `pkgVersion`. When both are specified, the API will check if agent(s) have ingested data for only that package's datastreams. ## UI walkthrough
🖼️ Click to show screenshots 1. **Integration policies** page now shows two tables for integrations meeting the above condition, one for agentless policies and one for agent-based policies: ![image](https://github.com/user-attachments/assets/58c6a932-9bda-4229-ba5f-d341bdbd539a) 2. Clicking the status badge in the agentless policies table opens a flyout with two steps: confirm agentless enrollment and confirm incoming data: ![image](https://github.com/user-attachments/assets/e19e6ba0-f40d-48a7-a524-0373934ac46a) 3. Confirm agentless enrollment polls for an agent enrolled into that integration policy's agent policy. If that agent is reporting an unhealthy status, the integration component UI is shown. This UI is the same one used on Fleet > Agents > Agent details page and shows all components reported by that agent: ![image](https://github.com/user-attachments/assets/ce214f7f-4bdd-48e5-a5eb-a1e8fcc7a512) 4. Once a healthy agentless enrollment is established, confirm incoming data starts polling for data for that integration ingested by that agent ID in the past 5 minutes: ![image](https://github.com/user-attachments/assets/7f3de40b-3418-4174-b529-e805407949b6) 5. If data could not be retrieved in 5 minutes, an error message shows while polling continues in the background: ![image](https://github.com/user-attachments/assets/a3fd198e-1570-4357-9b7f-e541a769d33f) 6. If data is retrieved, a success message is shown: ![image](https://github.com/user-attachments/assets/f4e442af-ca60-4448-9bfb-3f244cd03c2d)
## Testing Easiest way to test is use the Cloud deployment from this PR. Enable Beta integrations and navigate to CSPM. Add a CSPM integration using `Agentless` setup technology. Then you can track the status of the agentless deployment on the Integrations policies tab. For local testing, the following is required to simulate agentless agent: 1. Add the following to kibana.dev.yml: ``` xpack.cloud.id: 'anything-to-pass-cloud-validation-checks' xpack.fleet.agentless.enabled: true xpack.fleet.agentless.api.url: 'https://localhost:8443' xpack.fleet.agentless.api.tls.certificate: './config/certs/ess-client.crt' xpack.fleet.agentless.api.tls.key: './config/certs/ess-client.key' xpack.fleet.agentless.api.tls.ca: './config/certs/ca.crt' ``` 2. Apply [this patch](https://gist.github.com/jen-huang/dfc3e02ceb63976ad54bd1f50c524cb4) to prevent attempt to create agentless pod 3. Enroll a Fleet Server as usual 4. Enable Beta integrations and navigate to CSPM. Add a CSPM integration using `Agentless` setup technology. 5. Enroll a normal Elastic Agent to the agent policy for that CSPM integration by using the token from Enrollment tokens ## To-do - [x] API tests - [x] Unit UI tests - [x] Manual Cloud tests - [x] File docs request - https://github.com/elastic/ingest-docs/issues/1466 - [ ] Update troubleshooting guide link once available ### Checklist Delete any items that are not applicable to this PR. - [x] 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 - [x] [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 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- oas_docs/bundle.json | 118 ++++ oas_docs/bundle.serverless.json | 118 ++++ oas_docs/output/kibana.serverless.yaml | 95 +++ oas_docs/output/kibana.yaml | 95 +++ .../current_fields.json | 2 + .../current_mappings.json | 6 + .../check_registered_types.test.ts | 4 +- .../fleet/common/constants/mappings.ts | 1 + ...plified_package_policy_helper.test.ts.snap | 1 + .../simplified_package_policy_helper.ts | 24 +- .../common/types/models/package_policy.ts | 1 + .../fleet/common/types/rest_spec/agent.ts | 2 + .../confirm_incoming_data_with_preview.tsx | 8 +- .../hooks/setup_technology.test.ts | 23 + .../hooks/setup_technology.ts | 28 +- .../single_page_layout/index.tsx | 1 + .../hooks/use_package_policy_steps.tsx | 1 + .../agent_details_integration.tsx | 206 ++++--- .../agent_details_integration_inputs.tsx | 37 +- .../agent_details_integrations.tsx | 4 +- .../agents/components/agent_health.tsx | 117 ++-- .../sections/epm/screens/detail/index.tsx | 2 +- .../components/agent_based_table.test.tsx | 126 ++++ .../policies/components/agent_based_table.tsx | 346 +++++++++++ .../components/agentless_table.test.tsx | 158 +++++ .../policies/components/agentless_table.tsx | 300 ++++++++++ .../detail/policies/package_policies.tsx | 564 +++++++----------- .../confirm_incoming_data.tsx | 2 +- .../use_get_agent_incoming_data.tsx | 39 +- .../index.test.tsx | 151 +++++ .../agentless_enrollment_flyout/index.tsx | 212 +++++++ .../step_confirm_data.tsx | 99 +++ .../step_confirm_enrollment.tsx | 151 +++++ .../plugins/fleet/public/components/index.ts | 1 + .../package_policy_actions_menu.test.tsx | 5 +- .../package_policy_actions_menu.tsx | 39 +- .../fleet/server/routes/agent/handlers.ts | 31 +- .../fleet/server/saved_objects/index.ts | 22 + .../fleet/server/services/agents/status.ts | 24 +- .../fleet/server/services/package_policy.ts | 1 + .../server/types/models/package_policy.ts | 20 + .../fleet/server/types/rest_spec/agent.ts | 2 + .../agentless/create_agent.ts | 8 +- .../add_cis_integration_form_page.ts | 22 +- .../apis/agents/status.ts | 26 + .../agentless_api/create_agent.ts | 6 +- 46 files changed, 2659 insertions(+), 590 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.test.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.tsx create mode 100644 x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.test.tsx create mode 100644 x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.tsx create mode 100644 x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_data.tsx create mode 100644 x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_enrollment.tsx diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json index 1d03129d262c2..b96aaa3a3ce00 100644 --- a/oas_docs/bundle.json +++ b/oas_docs/bundle.json @@ -6919,6 +6919,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -7943,6 +7949,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -8736,6 +8748,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -9790,6 +9808,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -10813,6 +10837,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -11607,6 +11637,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -12744,6 +12780,22 @@ ] } }, + { + "in": "query", + "name": "pkgName", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "pkgVersion", + "required": false, + "schema": { + "type": "string" + } + }, { "in": "query", "name": "previewData", @@ -30076,6 +30128,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -30563,6 +30621,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -30800,6 +30864,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -31336,6 +31406,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -32038,6 +32114,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33210,6 +33292,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33622,6 +33710,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -34314,6 +34408,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -34808,6 +34908,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -35044,6 +35150,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -35579,6 +35691,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json index 5a1e2b69438e1..9115670ecb313 100644 --- a/oas_docs/bundle.serverless.json +++ b/oas_docs/bundle.serverless.json @@ -6919,6 +6919,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -7943,6 +7949,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -8736,6 +8748,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -9790,6 +9808,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -10813,6 +10837,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -11607,6 +11637,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -12744,6 +12780,22 @@ ] } }, + { + "in": "query", + "name": "pkgName", + "required": false, + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "pkgVersion", + "required": false, + "schema": { + "type": "string" + } + }, { "in": "query", "name": "previewData", @@ -30076,6 +30128,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -30563,6 +30621,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -30800,6 +30864,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -31336,6 +31406,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -32038,6 +32114,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33210,6 +33292,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -33622,6 +33710,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -34314,6 +34408,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, @@ -34808,6 +34908,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "additionalProperties": false, @@ -35044,6 +35150,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "vars": { "additionalProperties": { "anyOf": [ @@ -35579,6 +35691,12 @@ }, "type": "array" }, + "supports_agentless": { + "default": false, + "description": "Indicates whether the package policy belongs to an agentless agent policy.", + "nullable": true, + "type": "boolean" + }, "updated_at": { "type": "string" }, diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 33378690b6500..68a5ccaff2e10 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -9909,6 +9909,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -10618,6 +10623,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -11167,6 +11177,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -11696,6 +11711,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -12404,6 +12424,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -12953,6 +12978,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -13891,6 +13921,16 @@ paths: type: string type: array - type: string + - in: query + name: pkgName + required: false + schema: + type: string + - in: query + name: pkgVersion + required: false + schema: + type: string - in: query name: previewData required: false @@ -25537,6 +25577,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -25865,6 +25910,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -26015,6 +26065,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -26366,6 +26421,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -26826,6 +26886,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -27325,6 +27390,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -27655,6 +27725,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -27804,6 +27879,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -28154,6 +28234,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -28931,6 +29016,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -29210,6 +29300,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 6eb1e4c23f065..208bced5d70f6 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -12769,6 +12769,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -13477,6 +13482,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -14025,6 +14035,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -14553,6 +14568,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -15260,6 +15280,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -15808,6 +15833,11 @@ paths: required: - id type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -16739,6 +16769,16 @@ paths: type: string type: array - type: string + - in: query + name: pkgName + required: false + schema: + type: string + - in: query + name: pkgVersion + required: false + schema: + type: string - in: query name: previewData required: false @@ -28320,6 +28360,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -28647,6 +28692,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -28797,6 +28847,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -29148,6 +29203,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -29607,6 +29667,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -30104,6 +30169,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -30433,6 +30503,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false @@ -30582,6 +30657,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: anyOf: @@ -30932,6 +31012,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -31706,6 +31791,11 @@ paths: items: type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean updated_at: type: string updated_by: @@ -31985,6 +32075,11 @@ paths: description: Agent policy IDs where that package policy will be added type: string type: array + supports_agentless: + default: false + description: Indicates whether the package policy belongs to an agentless agent policy. + nullable: true + type: boolean vars: additionalProperties: additionalProperties: false diff --git a/packages/kbn-check-mappings-update-cli/current_fields.json b/packages/kbn-check-mappings-update-cli/current_fields.json index 1b1b847bb2747..45313933ab1b4 100644 --- a/packages/kbn-check-mappings-update-cli/current_fields.json +++ b/packages/kbn-check-mappings-update-cli/current_fields.json @@ -533,6 +533,7 @@ "revision", "secret_references", "secret_references.id", + "supports_agentless", "updated_at", "updated_by", "vars" @@ -715,6 +716,7 @@ "revision", "secret_references", "secret_references.id", + "supports_agentless", "updated_at", "updated_by", "vars" diff --git a/packages/kbn-check-mappings-update-cli/current_mappings.json b/packages/kbn-check-mappings-update-cli/current_mappings.json index 80739b3412e9b..17fb2c8cff1ed 100644 --- a/packages/kbn-check-mappings-update-cli/current_mappings.json +++ b/packages/kbn-check-mappings-update-cli/current_mappings.json @@ -1786,6 +1786,9 @@ } } }, + "supports_agentless": { + "type": "boolean" + }, "updated_at": { "type": "date" }, @@ -2374,6 +2377,9 @@ } } }, + "supports_agentless": { + "type": "boolean" + }, "updated_at": { "type": "date" }, diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index 61b92f4ccda64..fe08e3737800e 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -108,7 +108,7 @@ describe('checking migration metadata changes on all registered SO types', () => "fleet-agent-policies": "f57d3b70e4175a19a18f18ee72a379ceec82e1fc", "fleet-fleet-server-host": "69be15f6b6f2a2875ad3c7050ddea7a87f505417", "fleet-message-signing-keys": "93421f43fed2526b59092a4e3c65d64bc2266c0f", - "fleet-package-policies": "8be2cabfed89e103e0d413f2900e9cf6cd31bc68", + "fleet-package-policies": "0206c20f27286787b91814a2e7872f06dc1e8e47", "fleet-preconfiguration-deletion-record": "c52ea1e13c919afe8a5e8e3adbb7080980ecc08e", "fleet-proxy": "6cb688f0d2dd856400c1dbc998b28704ff70363d", "fleet-setup-lock": "0dc784792c79b5af5a6e6b5dcac06b0dbaa90bde", @@ -124,7 +124,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-agent-policies": "5e95e539826a40ad08fd0c1d161da0a4d86ffc6d", "ingest-download-sources": "279a68147e62e4d8858c09ad1cf03bd5551ce58d", "ingest-outputs": "55988d5f778bbe0e76caa7e6468707a0a056bdd8", - "ingest-package-policies": "dfa7b1045a2667a822181f40f012786724492439", + "ingest-package-policies": "60d43f475f91417d14d9df05476acf2e63e99435", "ingest_manager_settings": "111a616eb72627c002029c19feb9e6c439a10505", "inventory-view": "b8683c8e352a286b4aca1ab21003115a4800af83", "kql-telemetry": "93c1d16c1a0dfca9c8842062cf5ef8f62ae401ad", diff --git a/x-pack/plugins/fleet/common/constants/mappings.ts b/x-pack/plugins/fleet/common/constants/mappings.ts index 6499da7f86cc9..f3d2b200cac58 100644 --- a/x-pack/plugins/fleet/common/constants/mappings.ts +++ b/x-pack/plugins/fleet/common/constants/mappings.ts @@ -72,6 +72,7 @@ export const PACKAGE_POLICIES_MAPPINGS = { properties: {}, }, secret_references: { properties: { id: { type: 'keyword' } } }, + supports_agentless: { type: 'boolean' }, revision: { type: 'integer' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, diff --git a/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap b/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap index 7c549b030a337..f2d4067be434b 100644 --- a/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap +++ b/x-pack/plugins/fleet/common/services/__snapshots__/simplified_package_policy_helper.test.ts.snap @@ -238,6 +238,7 @@ Object { "policy_ids": Array [ "policy123", ], + "supports_agentless": undefined, "vars": undefined, } `; diff --git a/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts b/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts index 14c7b3888f775..5e39f94959486 100644 --- a/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts +++ b/x-pack/plugins/fleet/common/services/simplified_package_policy_helper.ts @@ -49,6 +49,7 @@ export interface SimplifiedPackagePolicy { description?: string; vars?: SimplifiedVars; inputs?: SimplifiedInputs; + supports_agentless?: boolean | null; } export interface FormattedPackagePolicy extends Omit { @@ -154,18 +155,19 @@ export function simplifiedPackagePolicytoNewPackagePolicy( description, inputs = {}, vars: packageLevelVars, + supports_agentless: supportsAgentless, } = data; - const packagePolicy = packageToPackagePolicy( - packageInfo, - policyId && isEmpty(policyIds) ? policyId : policyIds, - namespace, - name, - description - ); - - if (outputId) { - packagePolicy.output_id = outputId; - } + const packagePolicy = { + ...packageToPackagePolicy( + packageInfo, + policyId && isEmpty(policyIds) ? policyId : policyIds, + namespace, + name, + description + ), + supports_agentless: supportsAgentless, + output_id: outputId, + }; if (packagePolicy.package && options?.experimental_data_stream_features) { packagePolicy.package.experimental_data_stream_features = diff --git a/x-pack/plugins/fleet/common/types/models/package_policy.ts b/x-pack/plugins/fleet/common/types/models/package_policy.ts index 354834d2571dc..5c98729ff6cd7 100644 --- a/x-pack/plugins/fleet/common/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/models/package_policy.ts @@ -93,6 +93,7 @@ export interface NewPackagePolicy { [key: string]: any; }; overrides?: { inputs?: { [key: string]: any } } | null; + supports_agentless?: boolean | null; } export interface UpdatePackagePolicy extends NewPackagePolicy { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts index b990e5367bb42..57e0cc4f735fe 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/agent.ts @@ -220,6 +220,8 @@ export interface GetAgentStatusResponse { export interface GetAgentIncomingDataRequest { query: { agentsIds: string[]; + pkgName?: string; + pkgVersion?: string; previewData?: boolean; }; } diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx index 1642abe05d72b..d6d4c3abd63f5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/confirm_incoming_data_with_preview.tsx @@ -131,11 +131,11 @@ export const ConfirmIncomingDataWithPreview: React.FunctionComponent = ({ setAgentDataConfirmed, troubleshootLink, }) => { - const { incomingData, dataPreview, isLoading, hasReachedTimeout } = usePollingIncomingData( + const { incomingData, dataPreview, isLoading, hasReachedTimeout } = usePollingIncomingData({ agentIds, - true, - MAX_AGENT_DATA_PREVIEW_COUNT - ); + previewData: true, + stopPollingAfterPreviewLength: MAX_AGENT_DATA_PREVIEW_COUNT, + }); const { enrolledAgents, numAgentsWithData } = useGetAgentIncomingData(incomingData, packageInfo); const isGuidedOnboardingActive = useIsGuidedOnboardingActive(packageInfo?.name); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts index ead6dd71dba79..1564d934b960e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.test.ts @@ -115,6 +115,7 @@ describe('useAgentless', () => { describe('useSetupTechnology', () => { const setNewAgentPolicy = jest.fn(); const updateAgentPoliciesMock = jest.fn(); + const updatePackagePolicyMock = jest.fn(); const setSelectedPolicyTabMock = jest.fn(); const newAgentPolicyMock = { name: 'mock_new_agent_policy', @@ -183,6 +184,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -212,6 +214,7 @@ describe('useSetupTechnology', () => { packagePolicy: packagePolicyMock, isEditPage: true, agentPolicies: [{ id: 'agentless-policy-id', supports_agentless: true } as any], + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -239,6 +242,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -248,6 +252,7 @@ describe('useSetupTechnology', () => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENTLESS); }); await waitFor(() => { + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-1', @@ -278,6 +283,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }; const { result, rerender } = renderHook((props = initialProps) => useSetupTechnology(props), { @@ -291,6 +297,7 @@ describe('useSetupTechnology', () => { }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); expect(setNewAgentPolicy).toHaveBeenCalledWith({ inactivity_timeout: 3600, name: 'Agentless policy for endpoint-1', @@ -306,9 +313,11 @@ describe('useSetupTechnology', () => { ...packagePolicyMock, name: 'endpoint-2', }, + updatePackagePolicy: updatePackagePolicyMock, }); await waitFor(() => { + expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); expect(setNewAgentPolicy).toHaveBeenCalledWith({ name: 'Agentless policy for endpoint-2', inactivity_timeout: 3600, @@ -332,6 +341,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -366,6 +376,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -376,12 +387,14 @@ describe('useSetupTechnology', () => { }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENTLESS); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); act(() => { result.current.handleSetupTechnologyChange(SetupTechnology.AGENT_BASED); }); expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: false }); await waitFor(() => { expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); @@ -397,6 +410,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -410,6 +424,7 @@ describe('useSetupTechnology', () => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); + expect(updatePackagePolicyMock).not.toHaveBeenCalled(); expect(setNewAgentPolicy).not.toHaveBeenCalled(); expect(setSelectedPolicyTabMock).not.toHaveBeenCalled(); }); @@ -435,6 +450,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -454,6 +470,7 @@ describe('useSetupTechnology', () => { supports_agentless: true, inactivity_timeout: 3600, }); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: true }); }); act(() => { @@ -463,6 +480,7 @@ describe('useSetupTechnology', () => { await waitFor(() => { expect(result.current.selectedSetupTechnology).toBe(SetupTechnology.AGENT_BASED); expect(setNewAgentPolicy).toHaveBeenCalledWith(newAgentPolicyMock); + expect(updatePackagePolicyMock).toHaveBeenCalledWith({ supports_agentless: false }); }); }); @@ -489,6 +507,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -533,6 +552,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -582,6 +602,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -627,6 +648,7 @@ describe('useSetupTechnology', () => { updateAgentPolicies: updateAgentPoliciesMock, setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); @@ -673,6 +695,7 @@ describe('useSetupTechnology', () => { setSelectedPolicyTab: setSelectedPolicyTabMock, packagePolicy: packagePolicyMock, packageInfo: packageInfoMock, + updatePackagePolicy: updatePackagePolicyMock, }) ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts index d785dd988ba3a..85f5cbdc5fae1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology.ts @@ -61,6 +61,7 @@ export function useSetupTechnology({ setNewAgentPolicy, newAgentPolicy, updateAgentPolicies, + updatePackagePolicy, setSelectedPolicyTab, packageInfo, packagePolicy, @@ -70,6 +71,7 @@ export function useSetupTechnology({ setNewAgentPolicy: (policy: NewAgentPolicy) => void; newAgentPolicy: NewAgentPolicy; updateAgentPolicies: (policies: AgentPolicy[]) => void; + updatePackagePolicy: (policy: Partial) => void; setSelectedPolicyTab: (tab: SelectedPolicyTab) => void; packageInfo?: PackageInfo; packagePolicy: NewPackagePolicy; @@ -112,16 +114,33 @@ export function useSetupTechnology({ updateAgentPolicies([nextNewAgentlessPolicy] as AgentPolicy[]); } } + if ( + selectedSetupTechnology === SetupTechnology.AGENTLESS && + !packagePolicy.supports_agentless + ) { + updatePackagePolicy({ + supports_agentless: true, + }); + } else if ( + selectedSetupTechnology !== SetupTechnology.AGENTLESS && + packagePolicy.supports_agentless + ) { + updatePackagePolicy({ + supports_agentless: false, + }); + } }, [ isAgentlessEnabled, isEditPage, newAgentlessPolicy, packagePolicy.name, + packagePolicy.supports_agentless, selectedSetupTechnology, updateAgentPolicies, setNewAgentPolicy, agentPolicies, setSelectedSetupTechnology, + updatePackagePolicy, ]); const handleSetupTechnologyChange = useCallback( @@ -142,11 +161,17 @@ export function useSetupTechnology({ setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([agentlessPolicy] as AgentPolicy[]); } + updatePackagePolicy({ + supports_agentless: true, + }); } else if (setupTechnology === SetupTechnology.AGENT_BASED) { setNewAgentPolicy({ ...newAgentBasedPolicy.current, supports_agentless: false, }); + updatePackagePolicy({ + supports_agentless: false, + }); setSelectedPolicyTab(SelectedPolicyTab.NEW); updateAgentPolicies([newAgentBasedPolicy.current] as AgentPolicy[]); } @@ -155,11 +180,12 @@ export function useSetupTechnology({ [ isAgentlessEnabled, selectedSetupTechnology, + updatePackagePolicy, setNewAgentPolicy, newAgentlessPolicy, + packageInfo, setSelectedPolicyTab, updateAgentPolicies, - packageInfo, ] ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx index 93631f63d0e04..3941c92a76c39 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/index.tsx @@ -355,6 +355,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({ newAgentPolicy, setNewAgentPolicy, updateAgentPolicies, + updatePackagePolicy, setSelectedPolicyTab, packageInfo, packagePolicy, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx index 1f2bdecf9e5ad..56f6a747dd574 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/hooks/use_package_policy_steps.tsx @@ -134,6 +134,7 @@ export function usePackagePolicySteps({ newAgentPolicy, setNewAgentPolicy, updateAgentPolicies, + updatePackagePolicy, setSelectedPolicyTab, packagePolicy, isEditPage: true, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx index db14401edfadb..84895a021df35 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration.tsx @@ -97,114 +97,122 @@ export const AgentDetailsIntegration: React.FunctionComponent<{ agent: Agent; agentPolicy: AgentPolicy; packagePolicy: PackagePolicy; + linkToLogs: boolean; 'data-test-subj'?: string; -}> = memo(({ agent, agentPolicy, packagePolicy, 'data-test-subj': dataTestSubj }) => { - const { getHref } = useLink(); - const theme = useEuiTheme(); +}> = memo( + ({ agent, agentPolicy, packagePolicy, linkToLogs = true, 'data-test-subj': dataTestSubj }) => { + const { getHref } = useLink(); + const theme = useEuiTheme(); - const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] = - useState(false); + const [isAttentionBadgeNeededForPolicyResponse, setIsAttentionBadgeNeededForPolicyResponse] = + useState(false); - const policyResponseExtensionView = useUIExtension( - packagePolicy.package?.name ?? '', - 'package-policy-response' - ); - - const policyResponseExtensionViewWrapper = useMemo(() => { - return ( - policyResponseExtensionView && ( - - - - ) - ); - }, [agent, policyResponseExtensionView]); - - const packageErrors = useMemo(() => { - if (!agent.components) { - return []; - } - return getInputUnitsByPackage(agent.components, packagePolicy).filter( - (u) => u.status === 'DEGRADED' || u.status === 'FAILED' + const policyResponseExtensionView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-policy-response' ); - }, [agent.components, packagePolicy]); - const showNeedsAttentionBadge = isAttentionBadgeNeededForPolicyResponse || !!packageErrors.length; + const policyResponseExtensionViewWrapper = useMemo(() => { + return ( + policyResponseExtensionView && ( + + + + ) + ); + }, [agent, policyResponseExtensionView]); + + const packageErrors = useMemo(() => { + if (!agent.components) { + return []; + } + return getInputUnitsByPackage(agent.components, packagePolicy).filter( + (u) => u.status === 'DEGRADED' || u.status === 'FAILED' + ); + }, [agent.components, packagePolicy]); - const genericErrorsListExtensionView = useUIExtension( - packagePolicy.package?.name ?? '', - 'package-generic-errors-list' - ); + const showNeedsAttentionBadge = + isAttentionBadgeNeededForPolicyResponse || !!packageErrors.length; - const genericErrorsListExtensionViewWrapper = useMemo(() => { - return ( - genericErrorsListExtensionView && ( - - - - ) + const genericErrorsListExtensionView = useUIExtension( + packagePolicy.package?.name ?? '', + 'package-generic-errors-list' ); - }, [packageErrors, genericErrorsListExtensionView]); - return ( - -

- - - {packagePolicy.package ? ( - - ) : ( - - )} - - - - {packagePolicy.name} - - - {showNeedsAttentionBadge && ( + const genericErrorsListExtensionViewWrapper = useMemo(() => { + return ( + genericErrorsListExtensionView && ( + + + + ) + ); + }, [packageErrors, genericErrorsListExtensionView]); + + return ( + +

+ - - - + ) : ( + + )} - )} - -

- - } - > - - {policyResponseExtensionViewWrapper} - {genericErrorsListExtensionViewWrapper} - -
- ); -}); + + + {packagePolicy.name} + + + {showNeedsAttentionBadge && ( + + + + + + )} +
+

+ + } + > + + {policyResponseExtensionViewWrapper} + {genericErrorsListExtensionViewWrapper} + +
+ ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx index 71b1a1ad6fe3e..7630b61c08be6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integration_inputs.tsx @@ -66,8 +66,9 @@ const StyledEuiTreeView = styled(EuiTreeView)` export const AgentDetailsIntegrationInputs: React.FunctionComponent<{ agent: Agent; packagePolicy: PackagePolicy; + linkToLogs?: boolean; 'data-test-subj'?: string; -}> = memo(({ agent, packagePolicy, 'data-test-subj': dataTestSubj }) => { +}> = memo(({ agent, packagePolicy, linkToLogs = true, 'data-test-subj': dataTestSubj }) => { const { getHref } = useLink(); const inputStatusMap = useMemo( @@ -138,21 +139,25 @@ export const AgentDetailsIntegrationInputs: React.FunctionComponent<{ defaultMessage: 'View logs', })} > - - {displayInputType(current.type)} - + {linkToLogs ? ( + + {displayInputType(current.type)} + + ) : ( + <>{displayInputType(current.type)} + )} ), id: current.type, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx index 5a7b135b46440..0a1df537e7bcd 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations.tsx @@ -15,7 +15,8 @@ import { AgentDetailsIntegration } from './agent_details_integration'; export const AgentDetailsIntegrations: React.FunctionComponent<{ agent: Agent; agentPolicy?: AgentPolicy; -}> = memo(({ agent, agentPolicy }) => { + linkToLogs?: boolean; +}> = memo(({ agent, agentPolicy, linkToLogs = true }) => { if (!agentPolicy || !agentPolicy.package_policies) { return null; } @@ -31,6 +32,7 @@ export const AgentDetailsIntegrations: React.FunctionComponent<{ agent={agent} agentPolicy={agentPolicy} packagePolicy={packagePolicy} + linkToLogs={linkToLogs} data-test-subj={`${testSubj}-accordion`} /> diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx index fa8e263c16170..c7b71af5f97b6 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx @@ -8,6 +8,7 @@ import React, { useMemo, useState } from 'react'; import styled from 'styled-components'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react'; +import type { EuiBadgeProps } from '@elastic/eui'; import { EuiBadge, EuiButton, @@ -34,67 +35,75 @@ import { useAgentRefresh } from '../agent_details_page/hooks'; import { AgentUpgradeAgentModal } from './agent_upgrade_modal'; -interface Props { +type Props = EuiBadgeProps & { agent: Agent; fromDetails?: boolean; -} - -const Status = { - Healthy: ( - - - - ), - Offline: ( - - - - ), - Inactive: ( - - - - ), - Unenrolled: ( - - - - ), - Unhealthy: ( - - - - ), - Updating: ( - - - - ), }; -function getStatusComponent(status: Agent['status']): React.ReactElement { +function getStatusComponent({ + status, + ...restOfProps +}: { + status: Agent['status']; +} & EuiBadgeProps): React.ReactElement { switch (status) { case 'error': case 'degraded': - return Status.Unhealthy; + return ( + + + + ); case 'inactive': - return Status.Inactive; + return ( + + + + ); case 'offline': - return Status.Offline; + return ( + + + + ); case 'unenrolling': case 'enrolling': case 'updating': - return Status.Updating; + return ( + + + + ); case 'unenrolled': - return Status.Unenrolled; + return ( + + + + ); default: - return Status.Healthy; + return ( + + + + ); } } @@ -102,7 +111,11 @@ const WrappedEuiCallOut = styled(EuiCallOut)` white-space: wrap !important; `; -export const AgentHealth: React.FunctionComponent = ({ agent, fromDetails }) => { +export const AgentHealth: React.FunctionComponent = ({ + agent, + fromDetails, + ...restOfProps +}) => { const { last_checkin: lastCheckIn, last_checkin_message: lastCheckInMessage } = agent; const msLastCheckIn = new Date(lastCheckIn || 0).getTime(); const lastCheckInMessageText = lastCheckInMessage ? ( @@ -172,14 +185,16 @@ export const AgentHealth: React.FunctionComponent = ({ agent, fromDetails > {isStuckInUpdating(agent) && !fromDetails ? (
- {getStatusComponent(agent.status)} + {getStatusComponent({ status: agent.status, ...restOfProps })}  
) : ( <> - {getStatusComponent(agent.status)} - {previousToOfflineStatus ? getStatusComponent(previousToOfflineStatus) : null} + {getStatusComponent({ status: agent.status, ...restOfProps })} + {previousToOfflineStatus + ? getStatusComponent({ status: previousToOfflineStatus, ...restOfProps }) + : null} )} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 9a707500bb03d..0e7e6117d2cf2 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -846,7 +846,7 @@ export function Detail() { {canReadIntegrationPolicies ? ( - + ) : ( { + it('renders the table with package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + + ); + await act(async () => { + expect(result.getByText('Integration policy')).toBeInTheDocument(); + expect(result.getByText('Package Policy 1')).toBeInTheDocument(); + expect(result.getByText('v1.0.0')).toBeInTheDocument(); + }); + }); + + it('shows loading message when isLoading is true', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + + ); + await act(async () => { + expect(result.getByText('Loading integration policies…')).toBeInTheDocument(); + }); + }); + + it('shows no policies message when there are no package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + + ); + await act(async () => { + expect(result.getByText('No integration policies')).toBeInTheDocument(); + }); + }); + + it('opens the agent enrollment flyout when add agent button is clicked', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + + ); + + await act(async () => { + fireEvent.click(screen.getByText('Add agent')); + }); + expect(result.getByTestId('agentEnrollmentFlyout')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.tsx new file mode 100644 index 0000000000000..197fa84bf4dd9 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agent_based_table.tsx @@ -0,0 +1,346 @@ +/* + * 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 { stringify, parse } from 'query-string'; +import React, { useEffect, useState } from 'react'; +import { useLocation, useHistory } from 'react-router-dom'; +import { + EuiBasicTable, + EuiLink, + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; + +import type { AgentPolicy, InMemoryPackagePolicy, PackagePolicy } from '../../../../../../types'; +import type { usePagination } from '../../../../../../hooks'; +import { useLink, useAuthz, useMultipleAgentPolicies } from '../../../../../../hooks'; +import { + AgentEnrollmentFlyout, + MultipleAgentPoliciesSummaryLine, + AgentPolicySummaryLine, + PackagePolicyActionsMenu, +} from '../../../../../../components'; + +import { Persona } from '../persona'; + +import { PackagePolicyAgentsCell } from './package_policy_agents_cell'; + +export const AgentBasedPackagePoliciesTable = ({ + isLoading, + packagePolicies, + packagePoliciesTotal, + refreshPackagePolicies, + pagination, + addAgentToPolicyIdFromParams, + showAddAgentHelpForPolicyId, +}: { + isLoading: boolean; + packagePolicies: Array<{ + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }>; + packagePoliciesTotal: number; + refreshPackagePolicies: () => void; + pagination: ReturnType; + addAgentToPolicyIdFromParams?: string | null; + showAddAgentHelpForPolicyId?: string | null; +}) => { + const { getHref } = useLink(); + const { search } = useLocation(); + const history = useHistory(); + + const [selectedTableIndex, setSelectedTableIndex] = useState(); + const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); + const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; + const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies; + const canReadAgentPolicies = useAuthz().fleet.readAgentPolicies; + const canShowMultiplePoliciesCell = + canUseMultipleAgentPolicies && canReadIntegrationPolicies && canReadAgentPolicies; + + // Show tour help for adding agents to a policy + const addAgentHelpForPolicyId = packagePolicies.find(({ agentPolicies }) => + agentPolicies.find((agentPolicy) => agentPolicy.id === showAddAgentHelpForPolicyId) + )?.packagePolicy?.id; + + // Handle the "add agent" link displayed in post-installation toast notifications in the case + // where a user is clicking the link while on the package policies listing page + const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState( + addAgentToPolicyIdFromParams || null + ); + useEffect(() => { + const unlisten = history.listen((location) => { + const params = new URLSearchParams(location.search); + const addAgentToPolicyId = params.get('addAgentToPolicyId'); + + if (addAgentToPolicyId) { + setFlyoutOpenForPolicyId(addAgentToPolicyId); + } + }); + + return () => unlisten(); + }, [history]); + + const selectedPolicies = + selectedTableIndex !== undefined ? packagePolicies[selectedTableIndex] : undefined; + const selectedAgentPolicies = selectedPolicies?.agentPolicies; + const selectedPackagePolicy = selectedPolicies?.packagePolicy; + const flyoutPolicy = selectedAgentPolicies?.length === 1 ? selectedAgentPolicies[0] : undefined; + + return ( + <> + + items={packagePolicies || []} + columns={[ + { + field: 'packagePolicy.name', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', { + defaultMessage: 'Integration policy', + }), + render(_, { agentPolicies, packagePolicy }) { + return ( + + {packagePolicy.name} + + ); + }, + }, + { + field: 'packagePolicy.package.version', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', { + defaultMessage: 'Version', + }), + render(_version, { agentPolicies, packagePolicy }) { + return ( + + + + + + + + {agentPolicies.length > 0 && packagePolicy.hasUpgrade && ( + + + + + + )} + + ); + }, + }, + { + field: 'packagePolicy.policy_ids', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentPolicy', { + defaultMessage: 'Agent policies', + }), + truncateText: true, + render(ids, { agentPolicies, packagePolicy }) { + return agentPolicies.length > 0 ? ( + canShowMultiplePoliciesCell ? ( + + ) : ( + + ) + ) : ids.length === 0 ? ( + + + + ) : ( + + +   + + + ); + }, + }, + { + field: 'packagePolicy.updated_by', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { + defaultMessage: 'Last updated by', + }), + truncateText: true, + render(updatedBy: PackagePolicy['updated_by']) { + return ; + }, + }, + { + field: 'packagePolicy.updated_at', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', { + defaultMessage: 'Last updated', + }), + truncateText: true, + render(updatedAt: PackagePolicy['updated_at']) { + return ( + + + + ); + }, + }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { + defaultMessage: 'Agents', + }), + render({ + agentPolicies, + packagePolicy, + rowIndex, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }) { + if (agentPolicies.length === 0) { + return ( + + + + ); + } + return ( + { + setSelectedTableIndex(rowIndex); + setFlyoutOpenForPolicyId(agentPolicies[0].id); + }} + hasHelpPopover={addAgentHelpForPolicyId === packagePolicy.id} + /> + ); + }, + }, + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { + defaultMessage: 'Actions', + }), + width: '8ch', + align: 'right', + render({ + agentPolicies, + packagePolicy, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + }) { + const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies + return ( + + ); + }, + }, + ]} + loading={isLoading} + data-test-subj="integrationPolicyTable" + pagination={{ + pageIndex: pagination.pagination.currentPage - 1, + pageSize: pagination.pagination.pageSize, + totalItemCount: packagePoliciesTotal, + pageSizeOptions: pagination.pageSizeOptions, + }} + onChange={({ page }: { page: { index: number; size: number } }) => { + pagination.setPagination({ + currentPage: page.index + 1, + pageSize: page.size, + }); + }} + noItemsMessage={ + isLoading ? ( + + ) : ( + + ) + } + /> + {flyoutOpenForPolicyId && selectedAgentPolicies && !isLoading && ( + { + setFlyoutOpenForPolicyId(null); + const { addAgentToPolicyId, ...rest } = parse(search); + history.replace({ search: stringify(rest) }); + }} + agentPolicy={flyoutPolicy} + selectedAgentPolicies={selectedAgentPolicies} + isIntegrationFlow={true} + installedPackagePolicy={{ + name: selectedPackagePolicy?.package?.name || '', + version: selectedPackagePolicy?.package?.version || '', + }} + /> + )} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.test.tsx new file mode 100644 index 0000000000000..25a9933fd7197 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.test.tsx @@ -0,0 +1,158 @@ +/* + * 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 React from 'react'; +import { fireEvent, act, waitFor } from '@testing-library/react'; + +import { AGENTS_PREFIX } from '../../../../../../../../../common/constants'; +import { sendGetAgents } from '../../../../../../hooks'; +import { createIntegrationsTestRendererMock } from '../../../../../../../../mock'; + +import { AgentlessPackagePoliciesTable } from './agentless_table'; + +jest.mock('../../../../../../hooks', () => ({ + ...jest.requireActual('../../../../../../hooks'), + useConfirmForceInstall: jest.fn(), + sendGetAgents: jest.fn(), +})); + +describe('AgentlessPackagePoliciesTable', () => { + const mockSendGetAgents = sendGetAgents as jest.MockedFunction; + + beforeEach(() => { + mockSendGetAgents.mockResolvedValue({ + data: { + items: [ + { + policy_id: 'policy1', + id: 'agent1', + packages: ['package'], + type: 'PERMANENT', + active: true, + enrolled_at: '2023-01-01T00:00:00Z', + local_metadata: {}, + status: 'online', + }, + ], + total: 1, + page: 1, + perPage: 10000, + }, + error: null, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const defaultProps = { + isLoading: false, + packagePolicies: [ + { + agentPolicies: [ + { + id: 'policy1', + name: 'Policy 1', + status: 'active' as const, + is_managed: false, + updated_at: '2023-01-01T00:00:00Z', + updated_by: 'user1', + namespace: 'default', + monitoring_enabled: [], + revision: 1, + is_protected: false, + }, + ], + packagePolicy: { + id: 'packagePolicy1', + name: 'Package Policy 1', + updated_by: 'user1', + updated_at: '2023-01-01T00:00:00Z', + inputs: [], + policy_id: 'policy1', + namespace: 'default', + enabled: true, + package: { + name: 'package', + title: 'Package', + version: '1.0.0', + }, + hasUpgrade: false, + revision: 1, + created_at: '2023-01-01T00:00:00Z', + created_by: 'user1', + policy_ids: ['policy1'], + }, + rowIndex: 0, + }, + ], + packagePoliciesTotal: 1, + refreshPackagePolicies: jest.fn(), + pagination: { + pagination: { currentPage: 1, pageSize: 10 }, + setPagination: jest.fn(), + pageSizeOptions: [10, 20, 50], + }, + }; + + it('shows loading message when isLoading is true', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + + ); + await act(async () => { + expect(result.getByText('Loading integration policies…')).toBeInTheDocument(); + }); + }); + + it('shows no items message when there are no package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render( + + ); + await act(async () => { + expect(result.getByText('No agentless integration policies')).toBeInTheDocument(); + }); + }); + + it('renders the table with package policies', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render(); + + await act(async () => { + expect(result.getByText('Package Policy 1')).toBeInTheDocument(); + expect(result.getByText('user1')).toBeInTheDocument(); + }); + }); + + it('displays agent health status when agents are loaded', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render(); + await waitFor(() => { + expect(mockSendGetAgents).toHaveBeenCalledWith({ + perPage: 10000, + kuery: `${AGENTS_PREFIX}.policy_id: "policy1"`, + }); + }); + expect(await result.findByText('Healthy')).toBeInTheDocument(); + }); + + it('opens flyout when status badge is clicked', async () => { + const renderer = createIntegrationsTestRendererMock(); + const result = renderer.render(); + await waitFor(() => { + expect(mockSendGetAgents).toHaveBeenCalledWith({ + perPage: 10000, + kuery: `${AGENTS_PREFIX}.policy_id: "policy1"`, + }); + }); + await act(async () => { + fireEvent.click(await result.findByText('Healthy')); + }); + expect(result.getByText('Confirm agentless enrollment')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.tsx new file mode 100644 index 0000000000000..22348f137c513 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/components/agentless_table.tsx @@ -0,0 +1,300 @@ +/* + * 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 React, { useEffect, useMemo, useState } from 'react'; +import type { HorizontalAlignment } from '@elastic/eui'; +import { EuiBadge, EuiBasicTable, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; + +import type { + Agent, + AgentPolicy, + InMemoryPackagePolicy, + PackagePolicy, +} from '../../../../../../types'; +import { AGENTS_PREFIX, SO_SEARCH_LIMIT } from '../../../../../../../../../common/constants'; +import type { usePagination } from '../../../../../../hooks'; +import { useLink, sendGetAgents, useAuthz, useStartServices } from '../../../../../../hooks'; +import { + Loading, + PackagePolicyActionsMenu, + AgentlessEnrollmentFlyout, +} from '../../../../../../components'; + +import { Persona } from '../persona'; +import { AgentHealth } from '../../../../../../../fleet/sections/agents/components'; + +const REFRESH_INTERVAL_MS = 30000; + +export const AgentlessPackagePoliciesTable = ({ + isLoading, + packagePolicies, + packagePoliciesTotal, + refreshPackagePolicies, + pagination, +}: { + isLoading: boolean; + packagePolicies: Array<{ + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }>; + packagePoliciesTotal: number; + refreshPackagePolicies: () => void; + pagination: ReturnType; +}) => { + const core = useStartServices(); + const { notifications } = core; + const authz = useAuthz(); + const { getHref } = useLink(); + const [isAgentsLoading, setIsAgentsLoading] = useState(false); + const [agentsByPolicyId, setAgentsByPolicyId] = useState>({}); + const canReadAgents = authz.fleet.readAgents; + + // Kuery for all agents enrolled into the agent policies associated with the package policies + // We use the first agent policy as agentless package policies have a 1:1 relationship with agent policies + // Maximum # of agent policies is 50, based on the max page size in UI + const agentsKuery = useMemo(() => { + return packagePolicies + .reduce((policyIds, { agentPolicies }) => { + return [...policyIds, ...(agentPolicies[0] ? [agentPolicies[0]?.id] : [])]; + }, [] as string[]) + .map((policyId) => `${AGENTS_PREFIX}.policy_id: "${policyId}"`) + .join(' or '); + }, [packagePolicies]); + + // Fetch agents using above kuery, if the user has access to read agents + // Polls every 30 seconds + useEffect(() => { + const fetchAgents = async () => { + const { data: agentsData, error } = await sendGetAgents({ + perPage: SO_SEARCH_LIMIT, + kuery: agentsKuery, + }); + + setAgentsByPolicyId( + (agentsData?.items || []).reduce((acc, agent) => { + if (agent.policy_id) { + acc[agent.policy_id] = agent; + } + return acc; + }, {} as Record) + ); + + if (error) { + notifications.toasts.addError(error, { + title: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusError', + { + defaultMessage: 'Error fetching agentless status information', + } + ), + }); + } + setIsAgentsLoading(false); + }; + + if (canReadAgents) { + setIsAgentsLoading(true); + fetchAgents(); + const interval = setInterval(() => { + fetchAgents(); + }, REFRESH_INTERVAL_MS); + return () => clearInterval(interval); + } + }, [agentsKuery, canReadAgents, notifications.toasts]); + + // Flyout state + const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState(); + const [flyoutPackagePolicy, setFlyoutPackagePolicy] = useState(); + const [flyoutAgentPolicy, setFlyoutAgentPolicy] = useState(); + + return ( + <> + + items={packagePolicies || []} + columns={[ + { + field: 'packagePolicy.name', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', { + defaultMessage: 'Integration policy', + }), + render(_, { agentPolicies, packagePolicy }) { + return ( + + {packagePolicy.name} + + ); + }, + }, + { + field: 'packagePolicy.updated_by', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { + defaultMessage: 'Last updated by', + }), + truncateText: true, + render(updatedBy: PackagePolicy['updated_by']) { + return ; + }, + }, + { + field: 'packagePolicy.updated_at', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', { + defaultMessage: 'Last updated', + }), + truncateText: true, + render(updatedAt: PackagePolicy['updated_at']) { + return ( + + + + ); + }, + }, + ...(canReadAgents + ? [ + { + field: '', + name: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatus', + { + defaultMessage: 'Status', + } + ), + align: 'left' as HorizontalAlignment, + render({ + agentPolicies, + packagePolicy, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }) { + if (isAgentsLoading) { + return ; + } + // Use the first agent policy ID associated with the package policy + // because agentless package policies are only associated with one agent policy + const agentPolicy = agentPolicies[0]; + const agent = + (agentPolicy?.id && agentsByPolicyId[agentPolicy.id]) || undefined; + + // Status badge click handler + const statusBadgeProps = { + onClick: () => { + setFlyoutOpenForPolicyId(packagePolicy.id); + setFlyoutPackagePolicy(packagePolicy); + setFlyoutAgentPolicy(agentPolicy); + }, + 'data-test-subj': 'agentlessStatusBadge', + onClickAriaLabel: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusAriaLabel', + { + defaultMessage: 'Open status details', + } + ), + }; + + return agent ? ( + + ) : ( + + + + ); + }, + }, + ] + : []), + { + field: '', + name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { + defaultMessage: 'Actions', + }), + width: '8ch', + align: 'right' as HorizontalAlignment, + render({ + agentPolicies, + packagePolicy, + }: { + agentPolicies: AgentPolicy[]; + packagePolicy: InMemoryPackagePolicy; + }) { + const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies + return ( + + ); + }, + }, + ]} + loading={isLoading} + data-test-subj="integrationPolicyTable" + pagination={{ + pageIndex: pagination.pagination.currentPage - 1, + pageSize: pagination.pagination.pageSize, + totalItemCount: packagePoliciesTotal, + pageSizeOptions: pagination.pageSizeOptions, + }} + onChange={({ page }: { page: { index: number; size: number } }) => { + pagination.setPagination({ + currentPage: page.index + 1, + pageSize: page.size, + }); + }} + noItemsMessage={ + isLoading ? ( + + ) : ( + + ) + } + /> + {flyoutOpenForPolicyId && flyoutPackagePolicy && ( + { + setFlyoutOpenForPolicyId(undefined); + setFlyoutPackagePolicy(undefined); + setFlyoutAgentPolicy(undefined); + }} + packagePolicy={flyoutPackagePolicy} + agentPolicy={flyoutAgentPolicy} + /> + )} + + ); +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx index 300de597f6900..06096b1c3de5b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/policies/package_policies.tsx @@ -4,80 +4,49 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { stringify, parse } from 'query-string'; -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { Redirect, useLocation, useHistory } from 'react-router-dom'; -import type { CriteriaWithPagination, EuiTableFieldDataColumnType } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Redirect, useLocation } from 'react-router-dom'; import { - EuiBasicTable, - EuiLink, + EuiAccordion, EuiFlexGroup, EuiFlexItem, + EuiNotificationBadge, + EuiPanel, + EuiSpacer, EuiText, - EuiButton, - EuiIcon, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedRelative, FormattedMessage } from '@kbn/i18n-react'; + +import { FormattedMessage } from '@kbn/i18n-react'; import { InstallStatus } from '../../../../../types'; -import type { GetAgentPoliciesResponseItem, InMemoryPackagePolicy } from '../../../../../types'; +import type { + AgentPolicy, + GetAgentPoliciesResponseItem, + InMemoryPackagePolicy, + PackageInfo, + PackagePolicy, +} from '../../../../../types'; import { useLink, - useUrlPagination, useGetPackageInstallStatus, AgentPolicyRefreshContext, useIsPackagePolicyUpgradable, - useAuthz, - useMultipleAgentPolicies, + usePagination, } from '../../../../../hooks'; import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; -import { - AgentEnrollmentFlyout, - MultipleAgentPoliciesSummaryLine, - AgentPolicySummaryLine, - PackagePolicyActionsMenu, -} from '../../../../../components'; import { SideBarColumn } from '../../../components/side_bar_column'; -import { PackagePolicyAgentsCell } from './components/package_policy_agents_cell'; -import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; -import { Persona } from './persona'; - -interface PackagePoliciesPanelProps { - name: string; - version: string; -} - -interface InMemoryPackagePolicyAndAgentPolicy { - packagePolicy: InMemoryPackagePolicy; - agentPolicies: GetAgentPoliciesResponseItem[]; - rowIndex: number; -} +import { useAgentless } from '../../../../../../fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/setup_technology'; -const IntegrationDetailsLink = memo<{ - packagePolicy: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']; - agentPolicies: InMemoryPackagePolicyAndAgentPolicy['agentPolicies']; -}>(({ packagePolicy }) => { - const { getHref } = useLink(); - return ( - - {packagePolicy.name} - - ); -}); +import { usePackagePoliciesWithAgentPolicy } from './use_package_policies_with_agent_policy'; +import { AgentBasedPackagePoliciesTable } from './components/agent_based_table'; +import { AgentlessPackagePoliciesTable } from './components/agentless_table'; -export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps) => { +export const PackagePoliciesPage = ({ packageInfo }: { packageInfo: PackageInfo }) => { + const { name, version } = packageInfo; const { search } = useLocation(); - const history = useHistory(); const queryParams = useMemo(() => new URLSearchParams(search), [search]); - const agentPolicyIdFromParams = useMemo( + const addAgentToPolicyIdFromParams = useMemo( () => queryParams.get('addAgentToPolicyId'), [queryParams] ); @@ -85,44 +54,27 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps () => queryParams.get('showAddAgentHelpForPolicyId'), [queryParams] ); - const [flyoutOpenForPolicyId, setFlyoutOpenForPolicyId] = useState( - agentPolicyIdFromParams - ); - const [selectedTableIndex, setSelectedTableIndex] = useState(); - - const { getPath, getHref } = useLink(); + const { getPath } = useLink(); const getPackageInstallStatus = useGetPackageInstallStatus(); const packageInstallStatus = getPackageInstallStatus(name); - const { pagination, pageSizeOptions, setPagination } = useUrlPagination(); - const { canUseMultipleAgentPolicies } = useMultipleAgentPolicies(); - const { - data, - isLoading, - resendRequest: refreshPolicies, - } = usePackagePoliciesWithAgentPolicy({ - page: pagination.currentPage, - perPage: pagination.pageSize, - kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: ${name}`, - }); const { isPackagePolicyUpgradable } = useIsPackagePolicyUpgradable(); + const { isAgentlessIntegration } = useAgentless(); + const canHaveAgentlessPolicies = useMemo( + () => isAgentlessIntegration(packageInfo), + [isAgentlessIntegration, packageInfo] + ); - const canWriteIntegrationPolicies = useAuthz().integrations.writeIntegrationPolicies; - const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies; - const canReadAgentPolicies = useAuthz().fleet.readAgentPolicies; - - const packageAndAgentPolicies = useMemo((): Array<{ - agentPolicies: GetAgentPoliciesResponseItem[]; - packagePolicy: InMemoryPackagePolicy; - rowIndex: number; - }> => { - if (!data?.items) { - return []; - } - - const newPolicies = data.items.map(({ agentPolicies, packagePolicy }, index) => { + // Helper function to map raw policies data for consumption by the table + const mapPoliciesData = useCallback( + ( + { + agentPolicies, + packagePolicy, + }: { agentPolicies: AgentPolicy[]; packagePolicy: PackagePolicy }, + index: number + ) => { const hasUpgrade = isPackagePolicyUpgradable(packagePolicy); - return { agentPolicies, packagePolicy: { @@ -131,284 +83,204 @@ export const PackagePoliciesPage = ({ name, version }: PackagePoliciesPanelProps }, rowIndex: index, }; - }); - - return newPolicies; - }, [data?.items, isPackagePolicyUpgradable]); - - const showAddAgentHelpForPackagePolicyId = packageAndAgentPolicies.find(({ agentPolicies }) => - agentPolicies.find((agentPolicy) => agentPolicy.id === showAddAgentHelpForPolicyId) - )?.packagePolicy?.id; - // Handle the "add agent" link displayed in post-installation toast notifications in the case - // where a user is clicking the link while on the package policies listing page - useEffect(() => { - const unlisten = history.listen((location) => { - const params = new URLSearchParams(location.search); - const addAgentToPolicyId = params.get('addAgentToPolicyId'); - - if (addAgentToPolicyId) { - setFlyoutOpenForPolicyId(addAgentToPolicyId); - } - }); - - return () => unlisten(); - }, [history]); - - const handleTableOnChange = useCallback( - ({ page }: CriteriaWithPagination) => { - setPagination({ - currentPage: page.index + 1, - pageSize: page.size, - }); }, - [setPagination] + [isPackagePolicyUpgradable] ); - const canShowMultiplePoliciesCell = - canUseMultipleAgentPolicies && canReadIntegrationPolicies && canReadAgentPolicies; - const columns: Array> = useMemo( - () => [ - { - field: 'packagePolicy.name', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.name', { - defaultMessage: 'Integration policy', - }), - render(_, { agentPolicies, packagePolicy }) { - return ( - - ); - }, - }, - { - field: 'packagePolicy.package.version', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.version', { - defaultMessage: 'Version', - }), - render(_version, { agentPolicies, packagePolicy }) { - return ( - - - - - - - {agentPolicies.length > 0 && packagePolicy.hasUpgrade && ( - - - - - - )} - - ); - }, - }, - { - field: 'packagePolicy.policy_ids', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentPolicy', { - defaultMessage: 'Agent policies', - }), - truncateText: true, - render(ids, { agentPolicies, packagePolicy }) { - return agentPolicies.length > 0 ? ( - canShowMultiplePoliciesCell ? ( - - ) : ( - - ) - ) : ids.length === 0 ? ( - - - - ) : ( - - -   - - - ); - }, - }, - { - field: 'packagePolicy.updated_by', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedBy', { - defaultMessage: 'Last updated by', - }), - truncateText: true, - render(updatedBy) { - return ; - }, - }, - { - field: 'packagePolicy.updated_at', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.updatedAt', { - defaultMessage: 'Last updated', - }), - truncateText: true, - render(updatedAt: InMemoryPackagePolicyAndAgentPolicy['packagePolicy']['updated_at']) { - return ( - - - - ); - }, - }, - { - field: '', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.agentCount', { - defaultMessage: 'Agents', - }), - render({ agentPolicies, packagePolicy, rowIndex }: InMemoryPackagePolicyAndAgentPolicy) { - if (agentPolicies.length === 0) { - return ( - - - - ); - } - return ( - { - setSelectedTableIndex(rowIndex); - setFlyoutOpenForPolicyId(agentPolicies[0].id); - }} - hasHelpPopover={showAddAgentHelpForPackagePolicyId === packagePolicy.id} - /> - ); - }, - }, - { - field: '', - name: i18n.translate('xpack.fleet.epm.packageDetails.integrationList.actions', { - defaultMessage: 'Actions', - }), - width: '8ch', - align: 'right', - render({ agentPolicies, packagePolicy }) { - const agentPolicy = agentPolicies[0]; // TODO: handle multiple agent policies - return ( - - ); - }, - }, - ], - [ - getHref, - canWriteIntegrationPolicies, - canShowMultiplePoliciesCell, - showAddAgentHelpForPackagePolicyId, - refreshPolicies, - ] - ); - - const noItemsMessage = useMemo(() => { - return isLoading ? ( - - ) : undefined; - }, [isLoading]); + // States and data for agent-based policies table + // If agentless is not supported or not an agentless integration, skip the + // conditional in the kuery + const { + pagination: agentBasedPagination, + pageSizeOptions: agentBasedPageSizeOptions, + setPagination: agentBasedSetPagination, + } = usePagination(); + const [agentBasedPackageAndAgentPolicies, setAgentBasedPackageAndAgentPolicies] = useState< + Array<{ + agentPolicies: GetAgentPoliciesResponseItem[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }> + >([]); + const { + data: agentBasedData, + isLoading: agentBasedIsLoading, + resendRequest: refreshAgentBasedPolicies, + } = usePackagePoliciesWithAgentPolicy({ + page: agentBasedPagination.currentPage, + perPage: agentBasedPagination.pageSize, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${name}" ${ + canHaveAgentlessPolicies + ? `AND NOT ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.supports_agentless: true` + : `` + }`, + }); + useEffect(() => { + setAgentBasedPackageAndAgentPolicies( + !agentBasedData?.items ? [] : agentBasedData.items.map(mapPoliciesData) + ); + }, [agentBasedData, mapPoliciesData]); - const tablePagination = useMemo(() => { - return { - pageIndex: pagination.currentPage - 1, - pageSize: pagination.pageSize, - totalItemCount: data?.total ?? 0, - pageSizeOptions, - }; - }, [data?.total, pageSizeOptions, pagination.currentPage, pagination.pageSize]); + // States and data for agentless policies table + // If agentless is not supported or not an agentless integration, this block and + // initial request is unnessary but reduces code complexity + const { + pagination: agentlessPagination, + pageSizeOptions: agentlessPageSizeOptions, + setPagination: agentlessSetPagination, + } = usePagination(); + const [agentlessPackageAndAgentPolicies, setAgentlessPackageAndAgentPolicies] = useState< + Array<{ + agentPolicies: GetAgentPoliciesResponseItem[]; + packagePolicy: InMemoryPackagePolicy; + rowIndex: number; + }> + >([]); + const { + data: agentlessData, + isLoading: agentlessIsLoading, + resendRequest: refreshAgentlessPolicies, + } = usePackagePoliciesWithAgentPolicy({ + page: agentlessPagination.currentPage, + perPage: agentlessPagination.pageSize, + kuery: `${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.package.name: "${name}" AND ${PACKAGE_POLICY_SAVED_OBJECT_TYPE}.supports_agentless: true`, + }); + useEffect(() => { + setAgentlessPackageAndAgentPolicies( + !agentlessData?.items ? [] : agentlessData.items.map(mapPoliciesData) + ); + }, [agentlessData, mapPoliciesData]); // if they arrive at this page and the package is not installed, send them to overview // this happens if they arrive with a direct url or they uninstall while on this tab - // Check flyoutOpenForPolicyId otherwise right after installing a new integration the flyout won't open - if (packageInstallStatus.status !== InstallStatus.installed && !flyoutOpenForPolicyId) { + // Check `addAgentToPolicyIdFromParams` otherwise right after installing a new integration the flyout won't open + if (packageInstallStatus.status !== InstallStatus.installed && !addAgentToPolicyIdFromParams) { return ( ); } - const selectedPolicies = - selectedTableIndex !== undefined ? packageAndAgentPolicies[selectedTableIndex] : undefined; - - const agentPolicies = selectedPolicies?.agentPolicies; - const packagePolicy = selectedPolicies?.packagePolicy; - const flyoutPolicy = agentPolicies?.length === 1 ? agentPolicies[0] : undefined; - return ( - + { + refreshAgentBasedPolicies(); + refreshAgentlessPolicies(); + }, + }} + > - + {!canHaveAgentlessPolicies ? ( + + ) : ( + <> + + + +

+ +

+
+
+ + +

{agentlessData?.total ?? 0}

+
+
+
+ } + > + + + + + + + + + +

+ +

+
+
+ + +

{agentBasedData?.total ?? 0}

+
+
+ + } + > + + + + +
+ + )} - {flyoutOpenForPolicyId && agentPolicies && !isLoading && ( - { - setFlyoutOpenForPolicyId(null); - const { addAgentToPolicyId, ...rest } = parse(search); - history.replace({ search: stringify(rest) }); - }} - agentPolicy={flyoutPolicy} - selectedAgentPolicies={agentPolicies} - isIntegrationFlow={true} - installedPackagePolicy={{ - name: packagePolicy?.package?.name || '', - version: packagePolicy?.package?.version || '', - }} - /> - )}
); }; diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx index 2a111e5d638a9..412b221e88d47 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/confirm_incoming_data.tsx @@ -30,7 +30,7 @@ export const ConfirmIncomingData: React.FunctionComponent = ({ setAgentDataConfirmed, troubleshootLink, }) => { - const { incomingData, isLoading } = usePollingIncomingData(agentIds); + const { incomingData, isLoading } = usePollingIncomingData({ agentIds }); const isGuidedOnboardingActive = useIsGuidedOnboardingActive(installedPolicy?.name); const { guidedOnboarding } = useStartServices(); diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx index 19034151d80a1..be08dbd186e5a 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/use_get_agent_incoming_data.tsx @@ -75,18 +75,26 @@ export const useGetAgentIncomingData = ( }; const POLLING_INTERVAL_MS = 5 * 1000; // 5 sec -const POLLING_TIMEOUT_MS = 5 * 60 * 1000; // 5 min +export const POLLING_TIMEOUT_MS = 5 * 60 * 1000; // 5 min /** - * Hook for polling incoming data for the selected agent policy. + * Hook for polling incoming data for the selected agent(s). * @param agentIds * @returns incomingData, isLoading */ -export const usePollingIncomingData = ( - agentIds: string[], - previewData?: boolean, - stopPollingAfterPreviewLength: number = 0 -) => { +export const usePollingIncomingData = ({ + agentIds, + pkgName, + pkgVersion, + previewData, + stopPollingAfterPreviewLength = 0, +}: { + agentIds: string[]; + pkgName?: string; + pkgVersion?: string; + previewData?: boolean; + stopPollingAfterPreviewLength?: number; +}) => { const timeout = useRef(undefined); const [result, setResult] = useState<{ incomingData: IncomingDataList[]; @@ -117,7 +125,12 @@ export const usePollingIncomingData = ( setHasReachedTimeout(true); } - const { data } = await sendGetAgentIncomingData({ agentsIds: agentIds, previewData }); + const { data } = await sendGetAgentIncomingData({ + agentsIds: agentIds, + previewData, + pkgName, + pkgVersion, + }); if (data?.items) { // filter out agents that have `data = false` and keep polling const filtered = data?.items.filter((item) => { @@ -153,7 +166,15 @@ export const usePollingIncomingData = ( return () => { isAborted = true; }; - }, [agentIds, result, previewData, stopPollingAfterPreviewLength, startedPollingAt]); + }, [ + agentIds, + result, + previewData, + stopPollingAfterPreviewLength, + startedPollingAt, + pkgName, + pkgVersion, + ]); return { ...result, diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.test.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.test.tsx new file mode 100644 index 0000000000000..ef3eb2b0da3cb --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.test.tsx @@ -0,0 +1,151 @@ +/* + * 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 React from 'react'; +import { act, waitFor } from '@testing-library/react'; + +import { sendGetAgents, useGetPackageInfoByKeyQuery } from '../../hooks'; +import { usePollingIncomingData } from '../agent_enrollment_flyout/use_get_agent_incoming_data'; +import { createIntegrationsTestRendererMock } from '../../mock'; + +import { AGENTS_PREFIX } from '../../constants'; + +import type { PackagePolicy } from '../../types'; + +import { AgentlessEnrollmentFlyout } from '.'; + +jest.mock('../../hooks', () => ({ + ...jest.requireActual('../../hooks'), + useGetPackageInfoByKeyQuery: jest.fn(), + sendGetAgents: jest.fn(), +})); + +jest.mock('../agent_enrollment_flyout/use_get_agent_incoming_data', () => ({ + usePollingIncomingData: jest.fn(), +})); + +const mockSendGetAgents = sendGetAgents as jest.Mock; +const mockUseGetPackageInfoByKeyQuery = useGetPackageInfoByKeyQuery as jest.Mock; +const mockUsePollingIncomingData = usePollingIncomingData as jest.Mock; + +describe('AgentlessEnrollmentFlyout', () => { + const onClose = jest.fn(); + const packagePolicy: PackagePolicy = { + id: 'test-package-policy-id', + name: 'test-package-policy', + namespace: 'default', + policy_ids: ['test-policy-id'], + policy_id: 'test-policy-id', + enabled: true, + output_id: '', + package: { name: 'test-package', title: 'Test Package', version: '1.0.0' }, + inputs: [{ enabled: true, policy_template: 'test-template', type: 'test-type', streams: [] }], + revision: 1, + created_at: '', + created_by: '', + updated_at: '', + updated_by: '', + }; + + beforeEach(() => { + mockSendGetAgents.mockResolvedValue({ data: { items: [] } }); + mockUseGetPackageInfoByKeyQuery.mockReturnValue({ data: { item: { title: 'Test Package' } } }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the flyout with initial loading state', async () => { + const renderer = createIntegrationsTestRendererMock(); + const { getByText } = renderer.render( + + ); + await act(async () => { + expect(getByText('Confirm agentless enrollment')).toBeInTheDocument(); + expect(getByText('Step 1 is loading')).toBeInTheDocument(); + expect( + getByText('Listening for agentless connection... this could take several minutes') + ).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is disabled')).toBeInTheDocument(); + }); + }); + + it('updates step statuses when agent deployment fails', async () => { + const renderer = createIntegrationsTestRendererMock(); + const agentData = { status: 'error' }; + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [agentData] } }); + + const { getByText } = renderer.render( + + ); + + await waitFor(() => { + expect(getByText('Confirm agentless enrollment')).toBeInTheDocument(); + expect(getByText('Step 1 has errors')).toBeInTheDocument(); + expect(getByText('Agentless deployment failed')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is disabled')).toBeInTheDocument(); + }); + }); + + it('fetches agents data on mount and sets step statuses when agent deployment succeeds', async () => { + const renderer = createIntegrationsTestRendererMock(); + const agentData = { status: 'online' }; + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [agentData] } }); + mockUsePollingIncomingData.mockReturnValue({ incomingData: [], hasReachedTimeout: false }); + + const { getByText } = renderer.render( + + ); + + await waitFor(() => { + expect(mockSendGetAgents).toHaveBeenCalledWith({ + kuery: `${AGENTS_PREFIX}.policy_id: "test-policy-id"`, + }); + expect(getByText('Confirm agentless enrollment')).toBeInTheDocument(); + expect(getByText('Step 1 is complete')).toBeInTheDocument(); + expect(getByText('Agentless deployment was successful')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is loading')).toBeInTheDocument(); + }); + }); + + it('shows confirm data step as failed when timeout has been reached', async () => { + const renderer = createIntegrationsTestRendererMock(); + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [{ status: 'online' }] } }); + mockUsePollingIncomingData.mockReturnValue({ incomingData: [], hasReachedTimeout: true }); + + const { getByText } = renderer.render( + + ); + + await waitFor(() => { + expect(getByText('Step 1 is complete')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 has errors')).toBeInTheDocument(); + expect(getByText('No incoming data received from agentless integration')).toBeInTheDocument(); + }); + }); + + it('shows confirm data step as successful when incoming data is received', async () => { + const renderer = createIntegrationsTestRendererMock(); + mockSendGetAgents.mockResolvedValueOnce({ data: { items: [{ status: 'online' }] } }); + mockUsePollingIncomingData.mockReturnValue({ incomingData: [{ data: 'test-data' }] }); + + const { getByText } = renderer.render( + + ); + + await waitFor(() => { + expect(getByText('Step 1 is complete')).toBeInTheDocument(); + expect(getByText('Confirm incoming data')).toBeInTheDocument(); + expect(getByText('Step 2 is complete')).toBeInTheDocument(); + expect(getByText('Incoming data received from agentless integration')).toBeInTheDocument(); + }); + }); +}); diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.tsx new file mode 100644 index 0000000000000..0fbf8d278fab8 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/index.tsx @@ -0,0 +1,212 @@ +/* + * 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 React, { useEffect, useMemo, useRef, useState } from 'react'; +import type { EuiStepStatus } from '@elastic/eui'; +import { + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiFlyoutFooter, + EuiSteps, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { AGENTS_PREFIX, MAX_FLYOUT_WIDTH } from '../../constants'; +import type { Agent, AgentPolicy, PackagePolicy } from '../../types'; +import { sendGetAgents, useStartServices, useGetPackageInfoByKeyQuery } from '../../hooks'; + +import { AgentlessStepConfirmEnrollment } from './step_confirm_enrollment'; +import { AgentlessStepConfirmData } from './step_confirm_data'; + +const REFRESH_INTERVAL_MS = 30000; + +/** + * This component displays additional status details of an agentless agent enrolled + * the chosen package policy (and its agent policy). + * It also displays confirmation that the agentless agent is ingesting data from + * the chosen package policy. + */ +export const AgentlessEnrollmentFlyout = ({ + onClose, + packagePolicy, + agentPolicy, +}: { + onClose: () => void; + packagePolicy: PackagePolicy; + agentPolicy?: AgentPolicy; +}) => { + const core = useStartServices(); + const { notifications } = core; + const [confirmEnrollmentStatus, setConfirmEnrollmentStatus] = useState('loading'); + const [confirmDataStatus, setConfirmDataStatus] = useState('disabled'); + const [agentData, setAgentData] = useState(); + + // Clear agent data polling + // Called when component is unmounted or when agent is healthy + const agentDataInterval = useRef(); + const clearAgentDataPolling = useMemo(() => { + return () => { + if (agentDataInterval.current) { + clearInterval(agentDataInterval.current); + } + }; + }, [agentDataInterval]); + + // Fetch agent(s) data for the first associated agent policy + // Polls every 30 seconds until agent is found and healthy + useEffect(() => { + const fetchAgents = async () => { + const { data: agentsData, error } = await sendGetAgents({ + kuery: `${AGENTS_PREFIX}.policy_id: "${packagePolicy.policy_ids[0]}"`, + }); + + if (error) { + notifications.toasts.addError(error, { + title: i18n.translate( + 'xpack.fleet.epm.packageDetails.integrationList.agentlessStatusError', + { + defaultMessage: 'Error fetching agentless status information', + } + ), + }); + } + + if (agentsData?.items?.[0]) { + setAgentData(agentsData.items?.[0]); + } + }; + + fetchAgents(); + agentDataInterval.current = setInterval(() => { + fetchAgents(); + }, REFRESH_INTERVAL_MS); + + return () => clearAgentDataPolling(); + }, [clearAgentDataPolling, notifications.toasts, packagePolicy.policy_ids]); + + // Watches agent data and updates step statuses and clears polling when agent is healthy + useEffect(() => { + if (agentData) { + if (agentData.status === 'online') { + setConfirmEnrollmentStatus('complete'); + setConfirmDataStatus('loading'); + clearAgentDataPolling(); + } else if (agentData.status === 'error' || agentData.status === 'degraded') { + setConfirmEnrollmentStatus('danger'); + setConfirmDataStatus('disabled'); + } else { + setConfirmEnrollmentStatus('loading'); + setConfirmDataStatus('disabled'); + } + } else { + setConfirmEnrollmentStatus('loading'); + setConfirmDataStatus('disabled'); + } + }, [agentData, clearAgentDataPolling]); + + // Calculate integration title from the base package info and what + // is configured on the package policy. + const { data: packageInfoData } = useGetPackageInfoByKeyQuery( + packagePolicy.package!.name, + packagePolicy.package!.version, + { + prerelease: true, + } + ); + + const integrationTitle = useMemo(() => { + if (packageInfoData?.item) { + const enabledInputs = packagePolicy.inputs?.filter((input) => input.enabled); + + // If only one input is enabled, find the input name from the package info and + // and use that for integration title. Otherwise, use the package name. + if (enabledInputs.length === 1 && enabledInputs[0].policy_template) { + const policyTemplate = packageInfoData.item.policy_templates?.find( + (template) => template.name === enabledInputs[0].policy_template + ); + const input = + policyTemplate && 'inputs' in policyTemplate + ? policyTemplate.inputs?.find((i) => i.type === enabledInputs[0].type) + : null; + return input?.title || packageInfoData.item.title; + } else { + return packageInfoData.item.title; + } + } + return packagePolicy.name; + }, [packageInfoData, packagePolicy]); + + return ( + + + +

{packagePolicy.name}

+
+
+ + + ), + status: confirmEnrollmentStatus, + }, + { + title: i18n.translate('xpack.fleet.agentlessEnrollmentFlyout.stepConfirmDataTitle', { + defaultMessage: 'Confirm incoming data', + }), + children: + agentData && confirmEnrollmentStatus === 'complete' ? ( + + ) : ( + <> // Avoids React error about null children prop + ), + status: confirmDataStatus, + }, + ]} + /> + + + + + + + + + + +
+ ); +}; diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_data.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_data.tsx new file mode 100644 index 0000000000000..34e69d8ef839a --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_data.tsx @@ -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 React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { EuiStepStatus } from '@elastic/eui'; +import { EuiText, EuiLink, EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import { useStartServices } from '../../hooks'; +import type { Agent, PackagePolicy } from '../../types'; +import { + usePollingIncomingData, + POLLING_TIMEOUT_MS, +} from '../agent_enrollment_flyout/use_get_agent_incoming_data'; + +export const AgentlessStepConfirmData = ({ + agent, + packagePolicy, + setConfirmDataStatus, +}: { + agent: Agent; + packagePolicy: PackagePolicy; + setConfirmDataStatus: (status: EuiStepStatus) => void; +}) => { + const { docLinks } = useStartServices(); + const [overallState, setOverallState] = useState<'pending' | 'success' | 'failure'>('pending'); + + // Fetch integration data for the given agent and package policy + const { incomingData, hasReachedTimeout } = usePollingIncomingData({ + agentIds: [agent.id], + pkgName: packagePolicy.package!.name, + pkgVersion: packagePolicy.package!.version, + }); + + // Calculate overall UI state from polling data + useEffect(() => { + if (incomingData.length > 0) { + setConfirmDataStatus('complete'); + setOverallState('success'); + } else if (hasReachedTimeout) { + setConfirmDataStatus('danger'); + setOverallState('failure'); + } else { + setConfirmDataStatus('loading'); + setOverallState('pending'); + } + }, [incomingData, hasReachedTimeout, setConfirmDataStatus]); + + if (overallState === 'success') { + return ( + + ); + } else if (overallState === 'failure') { + return ( + <> + + + +

+ + + + ), + }} + /> +

+
+ + ); + } + + return null; +}; diff --git a/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_enrollment.tsx b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_enrollment.tsx new file mode 100644 index 0000000000000..f8a5832e04875 --- /dev/null +++ b/x-pack/plugins/fleet/public/components/agentless_enrollment_flyout/step_confirm_enrollment.tsx @@ -0,0 +1,151 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiPanel, EuiText, EuiLink, EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import type { Agent, AgentPolicy } from '../../types'; +import { useStartServices } from '../../hooks'; +import { AgentDetailsIntegrations } from '../../applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_integrations'; + +export const AgentlessStepConfirmEnrollment = ({ + agent, + agentPolicy, + integrationTitle, +}: { + agent?: Agent; + agentPolicy?: AgentPolicy; + integrationTitle: string; +}) => { + const { docLinks } = useStartServices(); + const [overallState, setOverallState] = useState<'pending' | 'success' | 'failure'>('pending'); + + // Calculate overall UI state from agent status + useEffect(() => { + if (agent && agent.status === 'online') { + setOverallState('success'); + } else if (agent && (agent.status === 'error' || agent.status === 'degraded')) { + setOverallState('failure'); + } else { + setOverallState('pending'); + } + }, [agent]); + + if (overallState === 'success') { + return ( + <> + + + +

+ +

+
+ + ); + } else if (overallState === 'failure') { + return ( + <> + + {agent?.last_checkin_message &&

{agent.last_checkin_message}

} +
+ + +

+ + + + ), + }} + /> +

+
+ {agent && agentPolicy && ( + <> + + + + )} + + ); + } + + return ( + <> + + + + + + + +

+ + + + ), + policyPagePath: ( + + + + ), + }} + /> +

+
+ + ); +}; diff --git a/x-pack/plugins/fleet/public/components/index.ts b/x-pack/plugins/fleet/public/components/index.ts index 5a995ab1ecbce..39dcfa3e3cd62 100644 --- a/x-pack/plugins/fleet/public/components/index.ts +++ b/x-pack/plugins/fleet/public/components/index.ts @@ -29,3 +29,4 @@ export { HeaderReleaseBadge, InlineReleaseBadge } from './release_badge'; export { WithGuidedOnboardingTour } from './with_guided_onboarding_tour'; export { UninstallCommandFlyout } from './uninstall_command_flyout'; export { MultipleAgentPoliciesSummaryLine } from './multiple_agent_policy_summary_line'; +export { AgentlessEnrollmentFlyout } from './agentless_enrollment_flyout'; diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx index f016acf8783aa..b7759438986d5 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.test.tsx @@ -115,13 +115,12 @@ describe.skip('PackagePolicyActionsMenu', () => { useMultipleAgentPoliciesMock.mockReturnValue({ canUseMultipleAgentPolicies: false }); }); - it('Should disable upgrade button if package does not have upgrade', async () => { + it('Should not have upgrade button if package does not have upgrade', async () => { const agentPolicies = createMockAgentPolicies(); const packagePolicy = createMockPackagePolicy({ hasUpgrade: false }); const { utils } = renderMenu({ agentPolicies, packagePolicy }); await act(async () => { - const upgradeButton = utils.getByTestId('PackagePolicyActionsUpgradeItem'); - expect(upgradeButton).toBeDisabled(); + expect(utils.queryByTestId('PackagePolicyActionsUpgradeItem')).toBeNull(); }); }); diff --git a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx index e51d61ea8ab24..25ed040f05e79 100644 --- a/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx +++ b/x-pack/plugins/fleet/public/components/package_policy_actions_menu.tsx @@ -108,24 +108,27 @@ export const PackagePolicyActionsMenu: React.FunctionComponent<{ defaultMessage="Edit integration" /> , - - - , + ...(packagePolicy.hasUpgrade + ? [ + + + , + ] + : []), // FIXME: implement Copy package policy action // {}} key="packagePolicyCopy"> // = async (context, request, response) => { const coreContext = await context.core; const esClient = coreContext.elasticsearch.client.asCurrentUser; - - const returnDataPreview = request.query.previewData; - const agentIds = isStringArray(request.query.agentsIds) + const agentsIds = isStringArray(request.query.agentsIds) ? request.query.agentsIds : [request.query.agentsIds]; + const { pkgName, pkgVersion, previewData: returnDataPreview } = request.query; + + // If a package is specified, get data stream patterns for that package + // and scope incoming data query to that pattern + let dataStreamPattern: string | undefined; + if (pkgName && pkgVersion) { + const packageInfo = await getPackageInfo({ + savedObjectsClient: coreContext.savedObjects.client, + prerelease: true, + pkgName, + pkgVersion, + }); + dataStreamPattern = (packageInfo.data_streams || []) + .map((ds) => generateTemplateIndexPattern(ds)) + .join(','); + } - const { items, dataPreview } = await AgentService.getIncomingDataByAgentsId( + const { items, dataPreview } = await AgentService.getIncomingDataByAgentsId({ esClient, - agentIds, - returnDataPreview - ); + agentsIds, + dataStreamPattern, + returnDataPreview, + }); const body = { items, dataPreview }; diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 5173ada3f5418..0f196686a4719 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -614,6 +614,7 @@ export const getSavedObjectTypes = ( }, secret_references: { properties: { id: { type: 'keyword' } } }, overrides: { type: 'flattened', index: false }, + supports_agentless: { type: 'boolean' }, revision: { type: 'integer' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, @@ -774,6 +775,16 @@ export const getSavedObjectTypes = ( }, ], }, + '16': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + supports_agentless: { type: 'boolean' }, + }, + }, + ], + }, }, migrations: { '7.10.0': migratePackagePolicyToV7100, @@ -829,6 +840,7 @@ export const getSavedObjectTypes = ( }, secret_references: { properties: { id: { type: 'keyword' } } }, overrides: { type: 'flattened', index: false }, + supports_agentless: { type: 'boolean' }, revision: { type: 'integer' }, updated_at: { type: 'date' }, updated_by: { type: 'keyword' }, @@ -848,6 +860,16 @@ export const getSavedObjectTypes = ( }, ], }, + '2': { + changes: [ + { + type: 'mappings_addition', + addedMappings: { + supports_agentless: { type: 'boolean' }, + }, + }, + ], + }, }, }, [PACKAGES_SAVED_OBJECT_TYPE]: { diff --git a/x-pack/plugins/fleet/server/services/agents/status.ts b/x-pack/plugins/fleet/server/services/agents/status.ts index c413ee7e268c8..e960a7b955fea 100644 --- a/x-pack/plugins/fleet/server/services/agents/status.ts +++ b/x-pack/plugins/fleet/server/services/agents/status.ts @@ -177,11 +177,17 @@ export async function getAgentStatusForAgentPolicy( }; } -export async function getIncomingDataByAgentsId( - esClient: ElasticsearchClient, - agentsIds: string[], - returnDataPreview: boolean = false -) { +export async function getIncomingDataByAgentsId({ + esClient, + agentsIds, + dataStreamPattern = DATA_STREAM_INDEX_PATTERN, + returnDataPreview = false, +}: { + esClient: ElasticsearchClient; + agentsIds: string[]; + dataStreamPattern?: string; + returnDataPreview?: boolean; +}) { const logger = appContextService.getLogger(); try { @@ -189,7 +195,7 @@ export async function getIncomingDataByAgentsId( body: { index: [ { - names: [DATA_STREAM_INDEX_PATTERN], + names: [dataStreamPattern], privileges: ['read'], }, ], @@ -203,7 +209,7 @@ export async function getIncomingDataByAgentsId( const searchResult = await retryTransientEsErrors( () => esClient.search({ - index: DATA_STREAM_INDEX_PATTERN, + index: dataStreamPattern, allow_partial_search_results: true, _source: returnDataPreview, timeout: '5s', @@ -244,9 +250,9 @@ export async function getIncomingDataByAgentsId( if (!searchResult.aggregations?.agent_ids) { return { items: agentsIds.map((id) => { - return { items: { [id]: { data: false } } }; + return { [id]: { data: false } }; }), - data: [], + dataPreview: [], }; } diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 31747952173ce..eac31bb980256 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -1929,6 +1929,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { output_id: newPolicy.output_id, inputs: newPolicy.inputs[0]?.streams ? newPolicy.inputs : inputs, vars: newPolicy.vars || newPP.vars, + supports_agentless: newPolicy.supports_agentless, }; } } diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index c5f3e94868cf7..bbbb94b28cd4c 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -172,6 +172,16 @@ export const PackagePolicyBaseSchema = { ), ]) ), + supports_agentless: schema.maybe( + schema.nullable( + schema.boolean({ + defaultValue: false, + meta: { + description: 'Indicates whether the package policy belongs to an agentless agent policy.', + }, + }) + ) + ), }; export const NewPackagePolicySchema = schema.object({ @@ -286,6 +296,16 @@ export const SimplifiedPackagePolicyBaseSchema = schema.object({ output_id: schema.maybe(schema.oneOf([schema.literal(null), schema.string()])), vars: schema.maybe(SimplifiedVarsSchema), inputs: SimplifiedPackagePolicyInputsSchema, + supports_agentless: schema.maybe( + schema.nullable( + schema.boolean({ + defaultValue: false, + meta: { + description: 'Indicates whether the package policy belongs to an agentless agent policy.', + }, + }) + ) + ), }); export const SimplifiedPackagePolicyPreconfiguredSchema = SimplifiedPackagePolicyBaseSchema.extends( diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index f9db02b92a659..a5fb3e9e034ed 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -527,6 +527,8 @@ export const GetAgentStatusResponseSchema = schema.object({ export const GetAgentDataRequestSchema = { query: schema.object({ agentsIds: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), + pkgName: schema.maybe(schema.string()), + pkgVersion: schema.maybe(schema.string()), previewData: schema.boolean({ defaultValue: false }), }), }; diff --git a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts index 2065d1307fbda..8f48bbb7d1bf4 100644 --- a/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts +++ b/x-pack/test/cloud_security_posture_functional/agentless/create_agent.ts @@ -65,12 +65,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await cisIntegration.navigateToIntegrationCspList(); await pageObjects.header.waitUntilLoadingHasFinished(); - expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessIntegration()).to.be( integrationPolicyName ); - expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be( - `Agentless policy for ${integrationPolicyName}` - ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessStatus()).to.be('Pending'); }); it(`should show setup technology selector in edit mode`, async () => { @@ -97,7 +95,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await cisIntegration.navigateToIntegrationCspList(); await pageObjects.header.waitUntilLoadingHasFinished(); - await cisIntegration.navigateToEditIntegrationPage(); + await cisIntegration.navigateToEditAgentlessIntegrationPage(); await pageObjects.header.waitUntilLoadingHasFinished(); expect(await cisIntegration.showSetupTechnologyComponent()).to.be(true); diff --git a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts index 563507d705583..fe95c0887711d 100644 --- a/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts +++ b/x-pack/test/cloud_security_posture_functional/page_objects/add_cis_integration_form_page.ts @@ -218,6 +218,10 @@ export function AddCisIntegrationFormPageProvider({ await testSubjects.click('integrationNameLink'); }; + const navigateToEditAgentlessIntegrationPage = async () => { + await testSubjects.click('agentlessIntegrationNameLink'); + }; + const navigateToAddIntegrationKspmPage = async (space?: string) => { const options = space ? { @@ -485,7 +489,7 @@ export function AddCisIntegrationFormPageProvider({ await navigateToIntegrationCspList(); await PageObjects.header.waitUntilLoadingHasFinished(); - await navigateToEditIntegrationPage(); + await navigateToEditAgentlessIntegrationPage(); await PageObjects.header.waitUntilLoadingHasFinished(); // Fill out form to edit an agentless integration @@ -498,7 +502,7 @@ export function AddCisIntegrationFormPageProvider({ // Check if the Direct Access Key is updated package policy api with successful toast expect(await testSubjects.exists('policyUpdateSuccessToast')).to.be(true); - await navigateToEditIntegrationPage(); + await navigateToEditAgentlessIntegrationPage(); await PageObjects.header.waitUntilLoadingHasFinished(); }; @@ -511,12 +515,23 @@ export function AddCisIntegrationFormPageProvider({ return await integration.getVisibleText(); }; + const getFirstCspmIntegrationPageAgentlessIntegration = async () => { + const integration = await testSubjects.find('agentlessIntegrationNameLink'); + return await integration.getVisibleText(); + }; + const getFirstCspmIntegrationPageAgent = async () => { const agent = await testSubjects.find('agentPolicyNameLink'); // this is assuming that the agent was just created therefor should be the first element return await agent.getVisibleText(); }; + const getFirstCspmIntegrationPageAgentlessStatus = async () => { + const agent = await testSubjects.find('agentlessStatusBadge'); + // this is assuming that the agent was just created therefor should be the first element + return await agent.getVisibleText(); + }; + const getAgentBasedPolicyValue = async () => { const agentName = await testSubjects.find('createAgentPolicyNameField'); return await agentName.getAttribute('value'); @@ -568,10 +583,13 @@ export function AddCisIntegrationFormPageProvider({ testSubjectIds, inputIntegrationName, getFirstCspmIntegrationPageIntegration, + getFirstCspmIntegrationPageAgentlessIntegration, getFirstCspmIntegrationPageAgent, + getFirstCspmIntegrationPageAgentlessStatus, getAgentBasedPolicyValue, showSuccessfulToast, showSetupTechnologyComponent, navigateToEditIntegrationPage, + navigateToEditAgentlessIntegrationPage, }; } diff --git a/x-pack/test/fleet_api_integration/apis/agents/status.ts b/x-pack/test/fleet_api_integration/apis/agents/status.ts index 42b60a0624a96..4b56601078da2 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/status.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/status.ts @@ -334,5 +334,31 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxxx') .expect(400); }); + + it('should return incoming data status for specified agents', async () => { + // force install the system package to override package verification + await supertest + .post(`/api/fleet/epm/packages/system/1.50.0`) + .set('kbn-xsrf', 'xxxx') + .send({ force: true }) + .expect(200); + + const { body: apiResponse1 } = await supertest + .get(`/api/fleet/agent_status/data?agentsIds=agent1&agentsIds=agent2`) + .expect(200); + const { body: apiResponse2 } = await supertest + .get( + `/api/fleet/agent_status/data?agentsIds=agent1&agentsIds=agent2&pkgName=system&pkgVersion=1.50.0` + ) + .expect(200); + expect(apiResponse1).to.eql({ + items: [{ agent1: { data: false } }, { agent2: { data: false } }], + dataPreview: [], + }); + expect(apiResponse2).to.eql({ + items: [{ agent1: { data: false } }, { agent2: { data: false } }], + dataPreview: [], + }); + }); }); } diff --git a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts index 7611061398f18..017ef1b2fa82a 100644 --- a/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts +++ b/x-pack/test_serverless/functional/test_suites/security/ftr/cloud_security_posture/agentless_api/create_agent.ts @@ -78,12 +78,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await cisIntegration.navigateToIntegrationCspList(); await pageObjects.header.waitUntilLoadingHasFinished(); - expect(await cisIntegration.getFirstCspmIntegrationPageIntegration()).to.be( + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessIntegration()).to.be( integrationPolicyName ); - expect(await cisIntegration.getFirstCspmIntegrationPageAgent()).to.be( - `Agentless policy for ${integrationPolicyName}` - ); + expect(await cisIntegration.getFirstCspmIntegrationPageAgentlessStatus()).to.be('Pending'); }); it(`should create default agent-based agent`, async () => {