diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh
index 511f6ead2d43c..2704f894cf2b6 100755
--- a/.buildkite/scripts/common/env.sh
+++ b/.buildkite/scripts/common/env.sh
@@ -8,6 +8,7 @@ KIBANA_DIR=$(pwd)
export KIBANA_DIR
export XPACK_DIR="$KIBANA_DIR/x-pack"
+export XDG_CACHE_HOME="$HOME/.cache"
export CACHE_DIR="$HOME/.kibana"
export ES_CACHE_DIR="$HOME/.es-snapshot-cache"
PARENT_DIR="$(cd "$KIBANA_DIR/.."; pwd)"
@@ -110,7 +111,6 @@ export TEST_CORS_SERVER_PORT=6105
if [[ "$(which google-chrome-stable)" || "$(which google-chrome)" ]]; then
echo "Chrome detected, setting DETECT_CHROMEDRIVER_VERSION=true"
export DETECT_CHROMEDRIVER_VERSION=true
- export CHROMEDRIVER_FORCE_DOWNLOAD=true
else
echo "Chrome not detected, installing default chromedriver binary for the package version"
fi
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 56fe95cd65b39..c000628cf9c52 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -597,6 +597,7 @@ packages/kbn-management/settings/types @elastic/kibana-management
packages/kbn-management/settings/utilities @elastic/kibana-management
packages/kbn-management/storybook/config @elastic/kibana-management
test/plugin_functional/plugins/management_test_plugin @elastic/kibana-management
+packages/kbn-manifest @elastic/kibana-core
packages/kbn-mapbox-gl @elastic/kibana-presentation
x-pack/examples/third_party_maps_source_example @elastic/kibana-presentation
src/plugins/maps_ems @elastic/kibana-presentation
@@ -929,9 +930,9 @@ packages/kbn-test-eui-helpers @elastic/kibana-visualizations
x-pack/test/licensing_plugin/plugins/test_feature_usage @elastic/kibana-security
packages/kbn-test-jest-helpers @elastic/kibana-operations @elastic/appex-qa
packages/kbn-test-subj-selector @elastic/kibana-operations @elastic/appex-qa
-x-pack/test_serverless
-test
-x-pack/test
+x-pack/test_serverless
+test
+x-pack/test
x-pack/performance @elastic/appex-qa
x-pack/examples/testing_embedded_lens @elastic/kibana-visualizations
x-pack/examples/third_party_lens_navigation_prompt @elastic/kibana-visualizations
diff --git a/dev_docs/key_concepts/api_authorization.mdx b/dev_docs/key_concepts/api_authorization.mdx
new file mode 100644
index 0000000000000..b781808757c9a
--- /dev/null
+++ b/dev_docs/key_concepts/api_authorization.mdx
@@ -0,0 +1,319 @@
+---
+id: kibDevDocsSecurityAPIAuthorization
+slug: /kibana-dev-docs/key-concepts/security-api-authorization
+title: Kibana API authorization
+description: This guide provides an overview of API authorization in Kibana.
+date: 2024-10-04
+tags: ['kibana', 'dev', 'contributor', 'security']
+---
+
+Authorization is an important aspect of API design. It must be considered for all endpoints, even those marked as `internal`. This guide explains how and when to apply authorization to your endpoints
+
+Table of contents:
+1. [API authorization](#api-authorization)
+2. [[Deprecated] Adding API authorization with `access` tags](#deprecated-adding-api-authorization-with-access-tags)
+ - [Why not add `access` tags to all routes by default?](#why-not-add-access-tags-to-all-routes-by-default)
+3. [Adding API authorization with `security` configuration](#adding-api-authorization-with-security-configuration)
+ - [Key features](#key-features)
+ - [Configuring authorization on routes](#configuring-authorization-on-routes)
+ - [Opting out of authorization for specific routes](#opting-out-of-authorization-for-specific-routes)
+ - [Classic router security configuration examples](#classic-router-security-configuration-examples)
+ - [Versioned router security configuration examples](#versioned-router-security-configuration-examples)
+4. [Authorization response available in route handlers](#authorization-response-available-in-route-handlers)
+5. [OpenAPI specification (OAS) documentation](#openapi-specification-oas-documentation)
+6. [Migrating from `access` tags to `security` configuration](#migrating-from-access-tags-to-security-configuration)
+7. [Questions?](#questions)
+
+## API authorization
+Kibana API routes do not have any authorization checks applied by default. This means that your APIs are accessible to anyone with valid credentials, regardless of their permissions. This includes users with no roles assigned.
+This on its own is insufficient, and care must be taken to ensure that only authorized users can invoke your endpoints.
+
+Kibana leverages for a majority of its persistence. The Saved Objects Service performs its own authorization checks, so if your API route is primarily a CRUD interface to Saved Objects, then your authorization needs are likely already met.
+This is also true for derivatives of the Saved Objects Service, such as the Alerting and Cases services.
+
+If your endpoint is not a CRUD interface to Saved Objects, or if your endpoint bypasses our built-in Saved Objects authorization checks, then you must ensure that only authorized users can invoke your endpoint.
+This is **especially** important if your route does any of the following:
+1. Performs non-insignificant processing, causing load on the Elasticsearch cluster or the Kibana server.
+2. Calls Elasticsearch APIs using the internal `kibana_system` user.
+3. Calls a third-party service.
+4. Exposes any non-public information to the caller, such as system configuration or state, as part of the successful or even error response.
+
+## [Deprecated] Adding API authorization with `access` tags
+**Note**: `access` tags were deprecated in favour of `security` configuration.
+
+`access` tags are used to restrict access to API routes. They are used to ensure that only users with the required privileges can access the route.
+
+Example configuration:
+```ts
+router.get({
+ path: '/api/path',
+ options: {
+ tags: ['access:', 'access:'],
+ },
+ ...
+}, handler);
+```
+
+More information on adding `access` tags to your routes can be found temporarily in the [legacy documentation](https://www.elastic.co/guide/en/kibana/current/development-security.html#development-plugin-feature-registration)
+
+### Why not add `access` tags to all routes by default?
+Each authorization check that we perform involves a round-trip to Elasticsearch, so they are not as cheap as we'd like. Therefore, we want to keep the number of authorization checks we perform within a single route to a minimum.
+Adding an `access` tag to routes that leverage the Saved Objects Service would be redundant in most cases, since the Saved Objects Service will be performing authorization anyway.
+
+
+## Adding API authorization with `security` configuration
+`KibanaRouteOptions` provides a security configuration at the route definition level, offering robust security configurations for both **Classic** and **Versioned** routes.
+
+### Key features:
+1. **Fine-grained control**:
+ - Define the exact privileges required to access the route.
+ - Use `requiredPrivileges` to specify privileges with support for complex rules:
+ - **AND rules** using `allRequired`: Requires all specified privileges for access.
+ - **OR rules** using `anyRequired`: Allows access if any one of the specified privileges is met.
+ - **Complex Nested Rules**: Combine both `allRequired` and `anyRequired` for advanced access rules.
+2. **Explicit Opt-out**: Provide a reason for opting out of authorization to maintain transparency.
+3. **Versioned Routes**: Define security configurations for different versions of the same route.
+4. **Improved Documentation with OpenAPI (OAS)**: Automatically generated OAS documentation with the required privileges for each route.
+5. **AuthzResult Object in Route Handlers**: Access the authorization response in route handlers to see which privileges were met.
+
+
+### Configuring authorization on routes
+**Before migration:**
+```ts
+router.get({
+ path: '/api/path',
+ options: {
+ tags: ['access:', 'access:'],
+ },
+ ...
+}, handler);
+```
+
+**After migration:**
+```ts
+router.get({
+ path: '/api/path',
+ security: {
+ authz: {
+ requiredPrivileges: ['', ''],
+ },
+ },
+ ...
+}, handler);
+```
+
+### Opting out of authorization for specific routes
+**Before migration:**
+```ts
+router.get({
+ path: '/api/path',
+ ...
+}, handler);
+```
+
+**After migration:**
+```ts
+router.get({
+ path: '/api/path',
+ security: {
+ authz: {
+ enabled: false,
+ reason: 'This route is opted out from authorization because ...',
+ },
+ },
+ ...
+}, handler);
+```
+
+### Classic router security configuration examples
+
+**Example 1: All privileges required.**
+Requires `` AND `` to access the route.
+```ts
+router.get({
+ path: '/api/path',
+ security: {
+ authz: {
+ requiredPrivileges: ['', ''],
+ },
+ },
+ ...
+}, handler);
+```
+
+**Example 2: Any privileges required.**
+Requires `` OR `` to access the route.
+```ts
+router.get({
+ path: '/api/path',
+ security: {
+ authz: {
+ requiredPrivileges: [{ anyRequired: ['', ''] }],
+ },
+ },
+ ...
+}, handler);
+```
+
+**Example 3: Complex configuration.**
+Requires `` AND `` AND (`` OR ``) to access the route.
+```ts
+router.get({
+ path: '/api/path',
+ security: {
+ authz: {
+ requiredPrivileges: [{ allRequired: ['', ''], anyRequired: ['', ''] }],
+ },
+ },
+ ...
+}, handler);
+```
+
+### Versioned router security configuration examples
+Different security configurations can be applied to each version when using the Versioned Router. This allows your authorization needs to evolve in lockstep with your API.
+
+**Example 1: Default and custom version security.**
+
+1. **Default configuration**: Applies to versions without specific authorization, requires ``.
+
+2. **Version 1**: Requires **both** `` and `` privileges.
+
+3. **Version 2**: Inherits the default authorization configuration, requiring ``.
+
+```ts
+router.versioned
+ .get({
+ path: '/internal/path',
+ access: 'internal',
+ // default security configuration, will be used for version unless overridden
+ security: {
+ authz: {
+ requiredPrivileges: [''],
+ },
+ },
+ })
+ .addVersion({
+ version: '1',
+ validate: false,
+ security: {
+ authz: {
+ requiredPrivileges: ['', ''],
+ },
+ },
+ }, handlerV1)
+ .addVersion({
+ version: '2',
+ validate: false,
+ }, handlerV2);
+```
+
+**Example 2: Multiple versions with different security requirements.**
+1. **Default Configuration**: Applies to versions without specific authorization, requires ``.
+
+2. **Version 1**: Requires **both** `` and `` privileges.
+
+3. **Version 2**: Requires `` AND (`` OR ``).
+
+4. **Version 3**: Requires only ``.
+
+```ts
+router.versioned
+ .get({
+ path: '/internal/path',
+ access: 'internal',
+ // default security configuration, will be used for version unless overridden
+ security: {
+ authz: {
+ requiredPrivileges: [''],
+ },
+ },
+ })
+ .addVersion({
+ version: '1',
+ validate: false,
+ security: {
+ authz: {
+ requiredPrivileges: ['', ''],
+ },
+ },
+ }, handlerV1)
+ .addVersion({
+ version: '2',
+ validate: false,
+ security: {
+ authz: {
+ requiredPrivileges: ['', anyRequired: ['', '']],
+ },
+ },
+ }, handlerV2)
+ .addVersion({
+ version: '3',
+ validate: false,
+ security: {
+ authz: {
+ requiredPrivileges: [''],
+ },
+ },
+ }, handlerV3);
+```
+
+## Authorization response available in route handlers
+The `AuthzResult` object is available in route handlers, which provides information about the privileges granted to the caller.
+For example, you have a route that requires `` and ANY of the privileges `` OR ``:
+```ts
+router.get({
+ path: '/api/path',
+ security: {
+ authz: {
+ requiredPrivileges: ['', { anyRequired: ['', ''] }],
+ },
+ },
+ ...
+}, (context, request, response) => {
+ // The authorization response is available in `request.authzResult`
+ // {
+ // "": true,
+ // "": true,
+ // "": false
+ // }
+});
+```
+
+## OpenAPI specification (OAS) documentation
+Based on the security configuration defined in routes, OAS documentation will automatically generate and include description about the required privileges.
+This makes it easy to view the security requirements of each endpoint in a standardized format, facilitating better understanding and usage by developers or teams consuming the API.
+
+To check the OAS documentation for a specific API route and see its security details, you can use the following command:
+```sh
+GET /api/oas?pathStartsWith=/your/api/path
+```
+
+## Migrating from `access` tags to `security` configuration
+We aim to use the same privileges that are currently defined with tags `access:`.
+To assist with this migration, we have created eslint rule `no_deprecated_authz_config`, that will automatically convert your `access` tags to the new `security` configuration.
+It scans route definitions and converts `access` tags to the new `requiredPriviliges` configuration.
+
+Note: The rule is disabled by default to avoid automatic migrations without an oversight. You can perform migrations by running:
+
+**Migrate routes with defined authorization**
+```sh
+MIGRATE_DISABLED_AUTHZ=false MIGRATE_ENABLED_AUTHZ=true npx eslint --ext .ts --fix path/to/your/folder
+```
+
+**Migrate routes opted out from authorization**
+```sh
+MIGRATE_DISABLED_AUTHZ=true MIGRATE_ENABLED_AUTHZ=false npx eslint --ext .ts --fix path/to/your/folder
+```
+We encourage you to migrate routes that are opted out from authorization to new config and provide legitimate reason for disabled authorization.
+It is better to migrate routes opted out from authorization iteratively and elaborate on the reasoning.
+Routes without a compelling reason to opt-out of authorization should plan to introduce them as soon as possible.
+
+**Migrate all routes**
+```sh
+MIGRATE_DISABLED_AUTHZ=true MIGRATE_ENABLED_AUTHZ=true npx eslint --ext .ts --fix path/to/your/folder
+```
+
+## Questions?
+If you have any questions or need help with API authorization, please reach out to the `@elastic/kibana-security` team.
+
+
diff --git a/dev_docs/key_concepts/security.mdx b/dev_docs/key_concepts/kibana_system_user.mdx
similarity index 62%
rename from dev_docs/key_concepts/security.mdx
rename to dev_docs/key_concepts/kibana_system_user.mdx
index 8e0bed133fe79..0373c8fa5d402 100644
--- a/dev_docs/key_concepts/security.mdx
+++ b/dev_docs/key_concepts/kibana_system_user.mdx
@@ -1,40 +1,12 @@
---
-id: kibDevDocsSecurityIntro
-slug: /kibana-dev-docs/key-concepts/security-intro
-title: Security
-description: Maintaining Kibana's security posture
-date: 2023-07-11
+id: kibDevDocsSecurityKibanaSystemUser
+slug: /kibana-dev-docs/key-concepts/security-kibana-system-user
+title: Security Kibana System User
+description: This guide provides an overview of `kibana_system` user
+date: 2024-10-04
tags: ['kibana', 'dev', 'contributor', 'security']
---
-Security is everyone's responsibility. This is inclusive of design, product, and engineering. The purpose of this guide is to give a high-level overview of security constructs and expectations.
-
-This guide covers the following topics:
-
-* [API authorization](#api-authorization)
-* [The `kibana_system` user](#the-kibana_system-user)
-
-## API authorization
-Kibana API routes do not have any authorization checks applied by default. This means that your APIs are accessible to anyone with valid credentials, regardless of their permissions. This includes users with no roles assigned.
-This on its own is insufficient, and care must be taken to ensure that only authorized users can invoke your endpoints.
-
-### Adding API authorization
-Kibana leverages for a majority of its persistence. The Saved Objects Service performs its own authorization checks, so if your API route is primarily a CRUD interface to Saved Objects, then your authorization needs are already met.
-This is also true for derivatives of the Saved Objects Service, such as the Alerting and Cases services.
-
-If your endpoint is not a CRUD interface to Saved Objects, then your route should include `access` tags to ensure that only authorized users can invoke your endpoint. This is **especially** important if your route does any of the following:
-1. Performs non-insignificant processing, causing load on the Kibana server.
-2. Calls Elasticsearch using the internal `kibana_system` user.
-3. Calls a third-party service.
-4. Returns any non-public information to the caller, such as system configuration or state.
-
-More information on adding `access` tags to your routes can be found temporarily in the [legacy documentation](https://www.elastic.co/guide/en/kibana/current/development-security.html#development-plugin-feature-registration)
-
-### Why not add `access` tags to all routes by default?
-Each authorization check that we perform involves a round-trip to Elasticsearch, so they are not as cheap as we'd like. Therefore, we want to keep the number of authorization checks we perform within a single route to a minimum.
-Adding an `access` tag to routes that leverage the Saved Objects Service would be redundant in most cases, since the Saved Objects Service will be performing authorization anyway.
-
-
## The `kibana_system` user
The Kibana server authenticates to Elasticsearch using the `elastic/kibana` [service account](https://www.elastic.co/guide/en/elasticsearch/reference/current/service-accounts.html#service-accounts-explanation). This service account has privileges that are equivilent to the `kibana_system` reserved role, whose descriptor is managed in the Elasticsearch repository ([source link](https://github.com/elastic/elasticsearch/blob/430cde6909eae12e1a90ac2bff29b71cbf4af18b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/KibanaOwnedReservedRoleDescriptors.java#L58)).
diff --git a/dev_docs/nav-kibana-dev.docnav.json b/dev_docs/nav-kibana-dev.docnav.json
index a7d696fc10574..6dd2ca052b7bd 100644
--- a/dev_docs/nav-kibana-dev.docnav.json
+++ b/dev_docs/nav-kibana-dev.docnav.json
@@ -101,7 +101,10 @@
"id": "kibBuildingBlocks"
},
{
- "id": "kibDevDocsSecurityIntro"
+ "id": "kibDevDocsSecurityAPIAuthorization"
+ },
+ {
+ "id": "kibDevDocsSecurityKibanaSystemUser"
},
{
"id": "kibDevFeaturePrivileges",
@@ -653,4 +656,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/oas_docs/bundle.json b/oas_docs/bundle.json
index 3d6c81c6eaa0b..6bca9024e77ea 100644
--- a/oas_docs/bundle.json
+++ b/oas_docs/bundle.json
@@ -9775,6 +9775,191 @@
]
}
},
+ "/api/fleet/agent_policies/outputs": {
+ "post": {
+ "description": "Get list of outputs associated with agent policies",
+ "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0",
+ "parameters": [
+ {
+ "description": "The version of the API to use",
+ "in": "header",
+ "name": "elastic-api-version",
+ "schema": {
+ "default": "2023-10-31",
+ "enum": [
+ "2023-10-31"
+ ],
+ "type": "string"
+ }
+ },
+ {
+ "description": "A required header to protect against CSRF attacks",
+ "in": "header",
+ "name": "kbn-xsrf",
+ "required": true,
+ "schema": {
+ "example": "true",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "ids": {
+ "description": "list of package policy ids",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "ids"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "items": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "agentPolicyId": {
+ "type": "string"
+ },
+ "data": {
+ "additionalProperties": false,
+ "properties": {
+ "integrations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "integrationPolicyName": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "pkgName": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ },
+ "monitoring": {
+ "additionalProperties": false,
+ "properties": {
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "monitoring",
+ "data"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "items"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "400": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "description": "Generic Error",
+ "properties": {
+ "error": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "summary": "",
+ "tags": [
+ "Elastic Agent policies"
+ ]
+ }
+ },
"/api/fleet/agent_policies/{agentPolicyId}": {
"get": {
"description": "Get an agent policy by ID",
@@ -12937,6 +13122,164 @@
]
}
},
+ "/api/fleet/agent_policies/{agentPolicyId}/outputs": {
+ "get": {
+ "description": "Get list of outputs associated with agent policy by policy id",
+ "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0",
+ "parameters": [
+ {
+ "description": "The version of the API to use",
+ "in": "header",
+ "name": "elastic-api-version",
+ "schema": {
+ "default": "2023-10-31",
+ "enum": [
+ "2023-10-31"
+ ],
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "agentPolicyId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "agentPolicyId": {
+ "type": "string"
+ },
+ "data": {
+ "additionalProperties": false,
+ "properties": {
+ "integrations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "integrationPolicyName": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "pkgName": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ },
+ "monitoring": {
+ "additionalProperties": false,
+ "properties": {
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "monitoring",
+ "data"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "item"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "400": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "description": "Generic Error",
+ "properties": {
+ "error": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "summary": "",
+ "tags": [
+ "Elastic Agent policies"
+ ]
+ }
+ },
"/api/fleet/agent_status": {
"get": {
"description": "Get agent status summary",
diff --git a/oas_docs/bundle.serverless.json b/oas_docs/bundle.serverless.json
index a6b3b71e8a804..a8d428d5404fc 100644
--- a/oas_docs/bundle.serverless.json
+++ b/oas_docs/bundle.serverless.json
@@ -9775,6 +9775,191 @@
]
}
},
+ "/api/fleet/agent_policies/outputs": {
+ "post": {
+ "description": "Get list of outputs associated with agent policies",
+ "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0",
+ "parameters": [
+ {
+ "description": "The version of the API to use",
+ "in": "header",
+ "name": "elastic-api-version",
+ "schema": {
+ "default": "2023-10-31",
+ "enum": [
+ "2023-10-31"
+ ],
+ "type": "string"
+ }
+ },
+ {
+ "description": "A required header to protect against CSRF attacks",
+ "in": "header",
+ "name": "kbn-xsrf",
+ "required": true,
+ "schema": {
+ "example": "true",
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "ids": {
+ "description": "list of package policy ids",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "ids"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "items": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "agentPolicyId": {
+ "type": "string"
+ },
+ "data": {
+ "additionalProperties": false,
+ "properties": {
+ "integrations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "integrationPolicyName": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "pkgName": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ },
+ "monitoring": {
+ "additionalProperties": false,
+ "properties": {
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "monitoring",
+ "data"
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ },
+ "required": [
+ "items"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "400": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "description": "Generic Error",
+ "properties": {
+ "error": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "summary": "",
+ "tags": [
+ "Elastic Agent policies"
+ ]
+ }
+ },
"/api/fleet/agent_policies/{agentPolicyId}": {
"get": {
"description": "Get an agent policy by ID",
@@ -12937,6 +13122,164 @@
]
}
},
+ "/api/fleet/agent_policies/{agentPolicyId}/outputs": {
+ "get": {
+ "description": "Get list of outputs associated with agent policy by policy id",
+ "operationId": "%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0",
+ "parameters": [
+ {
+ "description": "The version of the API to use",
+ "in": "header",
+ "name": "elastic-api-version",
+ "schema": {
+ "default": "2023-10-31",
+ "enum": [
+ "2023-10-31"
+ ],
+ "type": "string"
+ }
+ },
+ {
+ "in": "path",
+ "name": "agentPolicyId",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "properties": {
+ "item": {
+ "additionalProperties": false,
+ "properties": {
+ "agentPolicyId": {
+ "type": "string"
+ },
+ "data": {
+ "additionalProperties": false,
+ "properties": {
+ "integrations": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "integrationPolicyName": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "pkgName": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ },
+ "monitoring": {
+ "additionalProperties": false,
+ "properties": {
+ "output": {
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "name"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "output"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "monitoring",
+ "data"
+ ],
+ "type": "object"
+ }
+ },
+ "required": [
+ "item"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ },
+ "400": {
+ "content": {
+ "application/json; Elastic-Api-Version=2023-10-31": {
+ "schema": {
+ "additionalProperties": false,
+ "description": "Generic Error",
+ "properties": {
+ "error": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "statusCode": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "message"
+ ],
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "summary": "",
+ "tags": [
+ "Elastic Agent policies"
+ ]
+ }
+ },
"/api/fleet/agent_status": {
"get": {
"description": "Get agent status summary",
diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml
index 1b5199a641106..3f37d19f8e5e3 100644
--- a/oas_docs/output/kibana.serverless.staging.yaml
+++ b/oas_docs/output/kibana.serverless.staging.yaml
@@ -14594,6 +14594,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/{agentPolicyId}/outputs:
+ get:
+ description: Get list of outputs associated with agent policy by policy id
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - in: path
+ name: agentPolicyId
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ item:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ required:
+ - item
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@@ -14664,6 +14768,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/outputs:
+ post:
+ description: Get list of outputs associated with agent policies
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - description: A required header to protect against CSRF attacks
+ in: header
+ name: kbn-xsrf
+ required: true
+ schema:
+ example: 'true'
+ type: string
+ requestBody:
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ ids:
+ description: list of package policy ids
+ items:
+ type: string
+ type: array
+ required:
+ - ids
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ items:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ type: array
+ required:
+ - items
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary
diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml
index 1b5199a641106..3f37d19f8e5e3 100644
--- a/oas_docs/output/kibana.serverless.yaml
+++ b/oas_docs/output/kibana.serverless.yaml
@@ -14594,6 +14594,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/{agentPolicyId}/outputs:
+ get:
+ description: Get list of outputs associated with agent policy by policy id
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - in: path
+ name: agentPolicyId
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ item:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ required:
+ - item
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@@ -14664,6 +14768,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/outputs:
+ post:
+ description: Get list of outputs associated with agent policies
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - description: A required header to protect against CSRF attacks
+ in: header
+ name: kbn-xsrf
+ required: true
+ schema:
+ example: 'true'
+ type: string
+ requestBody:
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ ids:
+ description: list of package policy ids
+ items:
+ type: string
+ type: array
+ required:
+ - ids
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ items:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ type: array
+ required:
+ - items
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary
diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml
index 81c92add698eb..76ccd5ef75775 100644
--- a/oas_docs/output/kibana.staging.yaml
+++ b/oas_docs/output/kibana.staging.yaml
@@ -18023,6 +18023,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/{agentPolicyId}/outputs:
+ get:
+ description: Get list of outputs associated with agent policy by policy id
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - in: path
+ name: agentPolicyId
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ item:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ required:
+ - item
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@@ -18093,6 +18197,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/outputs:
+ post:
+ description: Get list of outputs associated with agent policies
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - description: A required header to protect against CSRF attacks
+ in: header
+ name: kbn-xsrf
+ required: true
+ schema:
+ example: 'true'
+ type: string
+ requestBody:
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ ids:
+ description: list of package policy ids
+ items:
+ type: string
+ type: array
+ required:
+ - ids
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ items:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ type: array
+ required:
+ - items
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary
diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml
index 81c92add698eb..76ccd5ef75775 100644
--- a/oas_docs/output/kibana.yaml
+++ b/oas_docs/output/kibana.yaml
@@ -18023,6 +18023,110 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/{agentPolicyId}/outputs:
+ get:
+ description: Get list of outputs associated with agent policy by policy id
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2F%7BagentPolicyId%7D%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - in: path
+ name: agentPolicyId
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ item:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ required:
+ - item
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_policies/delete:
post:
description: Delete agent policy by ID
@@ -18093,6 +18197,128 @@ paths:
summary: ''
tags:
- Elastic Agent policies
+ /api/fleet/agent_policies/outputs:
+ post:
+ description: Get list of outputs associated with agent policies
+ operationId: '%2Fapi%2Ffleet%2Fagent_policies%2Foutputs#0'
+ parameters:
+ - description: The version of the API to use
+ in: header
+ name: elastic-api-version
+ schema:
+ default: '2023-10-31'
+ enum:
+ - '2023-10-31'
+ type: string
+ - description: A required header to protect against CSRF attacks
+ in: header
+ name: kbn-xsrf
+ required: true
+ schema:
+ example: 'true'
+ type: string
+ requestBody:
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ ids:
+ description: list of package policy ids
+ items:
+ type: string
+ type: array
+ required:
+ - ids
+ responses:
+ '200':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ type: object
+ properties:
+ items:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ agentPolicyId:
+ type: string
+ data:
+ additionalProperties: false
+ type: object
+ properties:
+ integrations:
+ items:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ integrationPolicyName:
+ type: string
+ name:
+ type: string
+ pkgName:
+ type: string
+ type: array
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ monitoring:
+ additionalProperties: false
+ type: object
+ properties:
+ output:
+ additionalProperties: false
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ required:
+ - id
+ - name
+ required:
+ - output
+ required:
+ - monitoring
+ - data
+ type: array
+ required:
+ - items
+ '400':
+ content:
+ application/json; Elastic-Api-Version=2023-10-31:
+ schema:
+ additionalProperties: false
+ description: Generic Error
+ type: object
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ statusCode:
+ type: number
+ required:
+ - message
+ summary: ''
+ tags:
+ - Elastic Agent policies
/api/fleet/agent_status:
get:
description: Get agent status summary
diff --git a/package.json b/package.json
index 003047638d9d0..51d1b7472c6dc 100644
--- a/package.json
+++ b/package.json
@@ -635,6 +635,7 @@
"@kbn/management-settings-types": "link:packages/kbn-management/settings/types",
"@kbn/management-settings-utilities": "link:packages/kbn-management/settings/utilities",
"@kbn/management-test-plugin": "link:test/plugin_functional/plugins/management_test_plugin",
+ "@kbn/manifest": "link:packages/kbn-manifest",
"@kbn/mapbox-gl": "link:packages/kbn-mapbox-gl",
"@kbn/maps-custom-raster-source-plugin": "link:x-pack/examples/third_party_maps_source_example",
"@kbn/maps-ems-plugin": "link:src/plugins/maps_ems",
diff --git a/packages/core/http/core-http-server-internal/src/static_assets/util.ts b/packages/core/http/core-http-server-internal/src/static_assets/util.ts
index 9cd9213805b23..0bcc738582f2b 100644
--- a/packages/core/http/core-http-server-internal/src/static_assets/util.ts
+++ b/packages/core/http/core-http-server-internal/src/static_assets/util.ts
@@ -14,11 +14,23 @@ function isEmptyPathname(pathname: string): boolean {
}
function removeTailSlashes(pathname: string): string {
- return pathname.replace(/\/+$/, '');
+ let updated = pathname;
+
+ while (updated.endsWith('/')) {
+ updated = updated.substring(0, updated.length - 1);
+ }
+
+ return updated;
}
function removeLeadSlashes(pathname: string): string {
- return pathname.replace(/^\/+/, '');
+ let updated = pathname;
+
+ while (updated.startsWith('/')) {
+ updated = updated.substring(1);
+ }
+
+ return updated;
}
export function removeSurroundingSlashes(pathname: string): string {
diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts
index 9a93f50487bcd..73a0fc0659939 100644
--- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts
+++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.test.ts
@@ -8,6 +8,7 @@
*/
import {
+ hasAllKeywordsInOrder,
isClusterShardLimitExceeded,
isIncompatibleMappingException,
isIndexNotFoundException,
@@ -128,3 +129,31 @@ describe('isClusterShardLimitExceeded', () => {
expect(isClusterShardLimitExceeded(undefined)).toEqual(false);
});
});
+
+describe('hasAllKeywordsInOrder', () => {
+ it('returns false if not all keywords are present', () => {
+ expect(
+ hasAllKeywordsInOrder('some keywords in a message', ['some', 'in', 'message', 'missing'])
+ ).toEqual(false);
+ });
+
+ it('returns false if keywords are not in the right order', () => {
+ expect(
+ hasAllKeywordsInOrder('some keywords in a message', ['some', 'message', 'keywords'])
+ ).toEqual(false);
+ });
+
+ it('returns false if the message is empty', () => {
+ expect(hasAllKeywordsInOrder('', ['some', 'message', 'keywords'])).toEqual(false);
+ });
+
+ it('returns false if the keyword list is empty', () => {
+ expect(hasAllKeywordsInOrder('some keywords in a message', [])).toEqual(false);
+ });
+
+ it('returns true if keywords are present and in the right order', () => {
+ expect(
+ hasAllKeywordsInOrder('some keywords in a message', ['some', 'keywords', 'in', 'message'])
+ ).toEqual(true);
+ });
+});
diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts
index fbded8ad44b29..0ea6ccc227cba 100644
--- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts
+++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/actions/es_errors.ts
@@ -7,16 +7,16 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import type { ErrorCause } from '@elastic/elasticsearch/lib/api/types';
-export const isWriteBlockException = (errorCause?: estypes.ErrorCause): boolean => {
+export const isWriteBlockException = (errorCause?: ErrorCause): boolean => {
return (
errorCause?.type === 'cluster_block_exception' &&
- errorCause?.reason?.match(/index \[.+] blocked by: \[FORBIDDEN\/8\/.+ \(api\)\]/) !== null
+ hasAllKeywordsInOrder(errorCause?.reason, ['index [', '] blocked by: [FORBIDDEN/8/', ' (api)]'])
);
};
-export const isIncompatibleMappingException = (errorCause?: estypes.ErrorCause): boolean => {
+export const isIncompatibleMappingException = (errorCause?: ErrorCause): boolean => {
return (
errorCause?.type === 'strict_dynamic_mapping_exception' ||
errorCause?.type === 'mapper_parsing_exception' ||
@@ -24,17 +24,29 @@ export const isIncompatibleMappingException = (errorCause?: estypes.ErrorCause):
);
};
-export const isIndexNotFoundException = (errorCause?: estypes.ErrorCause): boolean => {
+export const isIndexNotFoundException = (errorCause?: ErrorCause): boolean => {
return errorCause?.type === 'index_not_found_exception';
};
-export const isClusterShardLimitExceeded = (errorCause?: estypes.ErrorCause): boolean => {
+export const isClusterShardLimitExceeded = (errorCause?: ErrorCause): boolean => {
// traditional ES: validation_exception. serverless ES: illegal_argument_exception
return (
(errorCause?.type === 'validation_exception' ||
errorCause?.type === 'illegal_argument_exception') &&
- errorCause?.reason?.match(
- /this action would add .* shards, but this cluster currently has .* maximum normal shards open/
- ) !== null
+ hasAllKeywordsInOrder(errorCause?.reason, [
+ 'this action would add',
+ 'shards, but this cluster currently has',
+ 'maximum normal shards open',
+ ])
);
};
+
+export const hasAllKeywordsInOrder = (message: string | undefined, keywords: string[]): boolean => {
+ if (!message || !keywords.length) {
+ return false;
+ }
+
+ const keywordIndices = keywords.map((keyword) => message?.indexOf(keyword) ?? -1);
+ // check that all keywords are present and in the right order
+ return keywordIndices.every((v, i, a) => v >= 0 && (!i || a[i - 1] <= v));
+};
diff --git a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts
index a201cfcd0e262..65f6735e22ca6 100644
--- a/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts
+++ b/packages/kbn-dev-utils/src/plugin_list/run_plugin_list_cli.ts
@@ -20,14 +20,39 @@ const OUTPUT_PATH = Path.resolve(REPO_ROOT, 'docs/developer/plugin-list.asciidoc
export function runPluginListCli() {
run(async ({ log }) => {
log.info('looking for oss plugins');
- const ossPlugins = discoverPlugins('src/plugins');
- log.success(`found ${ossPlugins.length} plugins`);
+ const ossLegacyPlugins = discoverPlugins('src/plugins');
+ const ossPlatformPlugins = discoverPlugins('src/platform/plugins');
+ log.success(`found ${ossLegacyPlugins.length + ossPlatformPlugins.length} plugins`);
log.info('looking for x-pack plugins');
- const xpackPlugins = discoverPlugins('x-pack/plugins');
- log.success(`found ${xpackPlugins.length} plugins`);
+ const xpackLegacyPlugins = discoverPlugins('x-pack/plugins');
+ const xpackPlatformPlugins = discoverPlugins('x-pack/platform/plugins');
+ const xpackSearchPlugins = discoverPlugins('x-pack/solutions/search/plugins');
+ const xpackSecurityPlugins = discoverPlugins('x-pack/solutions/security/plugins');
+ const xpackObservabilityPlugins = discoverPlugins('x-pack/solutions/observability/plugins');
+ log.success(
+ `found ${
+ xpackLegacyPlugins.length +
+ xpackPlatformPlugins.length +
+ xpackSearchPlugins.length +
+ xpackSecurityPlugins.length +
+ xpackObservabilityPlugins.length
+ } plugins`
+ );
log.info('writing plugin list to', OUTPUT_PATH);
- Fs.writeFileSync(OUTPUT_PATH, generatePluginList(ossPlugins, xpackPlugins));
+ Fs.writeFileSync(
+ OUTPUT_PATH,
+ generatePluginList(
+ [...ossLegacyPlugins, ...ossPlatformPlugins],
+ [
+ ...xpackLegacyPlugins,
+ ...xpackPlatformPlugins,
+ ...xpackSearchPlugins,
+ ...xpackSecurityPlugins,
+ ...xpackObservabilityPlugins,
+ ]
+ )
+ );
});
}
diff --git a/packages/kbn-eslint-config/.eslintrc.js b/packages/kbn-eslint-config/.eslintrc.js
index 4c429d3157fd9..ec39d88606438 100644
--- a/packages/kbn-eslint-config/.eslintrc.js
+++ b/packages/kbn-eslint-config/.eslintrc.js
@@ -317,6 +317,7 @@ module.exports = {
'@kbn/disable/no_naked_eslint_disable': 'error',
'@kbn/eslint/no_async_promise_body': 'error',
'@kbn/eslint/no_async_foreach': 'error',
+ '@kbn/eslint/no_deprecated_authz_config': 'error',
'@kbn/eslint/no_trailing_import_slash': 'error',
'@kbn/eslint/no_constructor_args_in_property_initializers': 'error',
'@kbn/eslint/no_this_in_property_initializers': 'error',
@@ -326,8 +327,8 @@ module.exports = {
'@kbn/imports/uniform_imports': 'error',
'@kbn/imports/no_unused_imports': 'error',
'@kbn/imports/no_boundary_crossing': 'error',
- '@kbn/eslint/no_deprecated_authz_config': 'error',
-
+ '@kbn/imports/no_group_crossing_manifests': 'error',
+ '@kbn/imports/no_group_crossing_imports': 'error',
'no-new-func': 'error',
'no-implied-eval': 'error',
'no-prototype-builtins': 'error',
diff --git a/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts b/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts
index 0eabafc48ab69..6e555f1d9527c 100644
--- a/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts
+++ b/packages/kbn-eslint-plugin-disable/src/helpers/protected_rules.ts
@@ -12,4 +12,6 @@ export const PROTECTED_RULES = new Set([
'@kbn/disable/no_protected_eslint_disable',
'@kbn/disable/no_naked_eslint_disable',
'@kbn/imports/no_unused_imports',
+ '@kbn/imports/no_group_crossing_imports',
+ '@kbn/imports/no_group_crossing_manifests',
]);
diff --git a/packages/kbn-eslint-plugin-imports/index.ts b/packages/kbn-eslint-plugin-imports/index.ts
index 9c57d66f60225..31e3483ea6139 100644
--- a/packages/kbn-eslint-plugin-imports/index.ts
+++ b/packages/kbn-eslint-plugin-imports/index.ts
@@ -13,6 +13,8 @@ import { UniformImportsRule } from './src/rules/uniform_imports';
import { ExportsMovedPackagesRule } from './src/rules/exports_moved_packages';
import { NoUnusedImportsRule } from './src/rules/no_unused_imports';
import { NoBoundaryCrossingRule } from './src/rules/no_boundary_crossing';
+import { NoGroupCrossingImportsRule } from './src/rules/no_group_crossing_imports';
+import { NoGroupCrossingManifestsRule } from './src/rules/no_group_crossing_manifests';
import { RequireImportRule } from './src/rules/require_import';
/**
@@ -25,5 +27,7 @@ export const rules = {
exports_moved_packages: ExportsMovedPackagesRule,
no_unused_imports: NoUnusedImportsRule,
no_boundary_crossing: NoBoundaryCrossingRule,
+ no_group_crossing_imports: NoGroupCrossingImportsRule,
+ no_group_crossing_manifests: NoGroupCrossingManifestsRule,
require_import: RequireImportRule,
};
diff --git a/packages/kbn-eslint-plugin-imports/src/helpers/groups.ts b/packages/kbn-eslint-plugin-imports/src/helpers/groups.ts
new file mode 100644
index 0000000000000..a76251f028389
--- /dev/null
+++ b/packages/kbn-eslint-plugin-imports/src/helpers/groups.ts
@@ -0,0 +1,25 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
+
+/**
+ * Checks whether a given ModuleGroup can import from another one
+ * @param importerGroup The group of the module that we are checking
+ * @param importedGroup The group of the imported module
+ * @param importedVisibility The visibility of the imported module
+ * @returns true if importerGroup is allowed to import from importedGroup/Visibiliy
+ */
+export function isImportableFrom(
+ importerGroup: ModuleGroup,
+ importedGroup: ModuleGroup,
+ importedVisibility: ModuleVisibility
+): boolean {
+ return importerGroup === importedGroup || importedVisibility === 'shared';
+}
diff --git a/packages/kbn-eslint-plugin-imports/src/helpers/report.ts b/packages/kbn-eslint-plugin-imports/src/helpers/report.ts
index 9ac0171507efd..11fc09fbecab3 100644
--- a/packages/kbn-eslint-plugin-imports/src/helpers/report.ts
+++ b/packages/kbn-eslint-plugin-imports/src/helpers/report.ts
@@ -30,3 +30,19 @@ export function report(context: Rule.RuleContext, options: ReportOptions) {
: null,
});
}
+
+export const toList = (strings: string[]) => {
+ const items = strings.map((s) => `"${s}"`);
+ const list = items.slice(0, -1).join(', ');
+ const last = items.at(-1);
+ return !list.length ? last ?? '' : `${list} or ${last}`;
+};
+
+export const formatSuggestions = (suggestions: string[]) => {
+ const s = suggestions.map((l) => l.trim()).filter(Boolean);
+ if (!s.length) {
+ return '';
+ }
+
+ return ` \nSuggestions:\n - ${s.join('\n - ')}\n\n`;
+};
diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts
index be9e60978fa88..f44c0571b2c94 100644
--- a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts
+++ b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.test.ts
@@ -9,8 +9,9 @@
import { RuleTester } from 'eslint';
import { NoBoundaryCrossingRule } from './no_boundary_crossing';
-import { ModuleType } from '@kbn/repo-source-classifier';
+import type { ModuleType } from '@kbn/repo-source-classifier';
import dedent from 'dedent';
+import { formatSuggestions } from '../helpers/report';
const make = (from: ModuleType, to: ModuleType, imp = 'import') => ({
filename: `${from}.ts`,
@@ -107,13 +108,12 @@ for (const [name, tester] of [tsTester, babelTester]) {
data: {
importedType: 'server package',
ownType: 'common package',
- suggestion: ` ${dedent`
- Suggestions:
- - Remove the import statement.
- - Limit your imports to "common package" or "static" code.
- - Covert to a type-only import.
- - Reach out to #kibana-operations for help.
- `}`,
+ suggestion: formatSuggestions([
+ 'Remove the import statement.',
+ 'Limit your imports to "common package" or "static" code.',
+ 'Covert to a type-only import.',
+ 'Reach out to #kibana-operations for help.',
+ ]),
},
},
],
diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts
index 59c73c1d0336c..3f426e13a6215 100644
--- a/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts
+++ b/packages/kbn-eslint-plugin-imports/src/rules/no_boundary_crossing.ts
@@ -12,13 +12,14 @@ import Path from 'path';
import { TSESTree } from '@typescript-eslint/typescript-estree';
import * as Bt from '@babel/types';
import type { Rule } from 'eslint';
-import ESTree from 'estree';
-import { ModuleType } from '@kbn/repo-source-classifier';
+import type { Node } from 'estree';
+import type { ModuleType } from '@kbn/repo-source-classifier';
import { visitAllImportStatements, Importer } from '../helpers/visit_all_import_statements';
import { getSourcePath } from '../helpers/source';
import { getRepoSourceClassifier } from '../helpers/repo_source_classifier';
import { getImportResolver } from '../get_import_resolver';
+import { formatSuggestions, toList } from '../helpers/report';
const ANY = Symbol();
@@ -33,22 +34,6 @@ const IMPORTABLE_FROM: Record = {
tooling: ANY,
};
-const toList = (strings: string[]) => {
- const items = strings.map((s) => `"${s}"`);
- const list = items.slice(0, -1).join(', ');
- const last = items.at(-1);
- return !list.length ? last ?? '' : `${list} or ${last}`;
-};
-
-const formatSuggestions = (suggestions: string[]) => {
- const s = suggestions.map((l) => l.trim()).filter(Boolean);
- if (!s.length) {
- return '';
- }
-
- return ` Suggestions:\n - ${s.join('\n - ')}`;
-};
-
const isTypeOnlyImport = (importer: Importer) => {
// handle babel nodes
if (Bt.isImportDeclaration(importer)) {
@@ -125,7 +110,7 @@ export const NoBoundaryCrossingRule: Rule.RuleModule = {
if (!importable.includes(imported.type)) {
context.report({
- node: node as ESTree.Node,
+ node: node as Node,
messageId: 'TYPE_MISMATCH',
data: {
ownType: self.type,
diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.test.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.test.ts
new file mode 100644
index 0000000000000..dc4828603f73f
--- /dev/null
+++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.test.ts
@@ -0,0 +1,155 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { RuleTester } from 'eslint';
+import dedent from 'dedent';
+import { NoGroupCrossingImportsRule } from './no_group_crossing_imports';
+import { formatSuggestions } from '../helpers/report';
+import { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
+
+const make = (
+ fromGroup: ModuleGroup,
+ fromVisibility: ModuleVisibility,
+ toGroup: ModuleGroup,
+ toVisibility: ModuleVisibility,
+ imp = 'import'
+) => ({
+ filename: `${fromGroup}.${fromVisibility}.ts`,
+ code: dedent`
+ ${imp} '${toGroup}.${toVisibility}'
+ `,
+});
+
+jest.mock('../get_import_resolver', () => {
+ return {
+ getImportResolver() {
+ return {
+ resolve(req: string) {
+ return {
+ type: 'file',
+ absolute: req.split('.'),
+ };
+ },
+ };
+ },
+ };
+});
+
+jest.mock('../helpers/repo_source_classifier', () => {
+ return {
+ getRepoSourceClassifier() {
+ return {
+ classify(r: string | [string, string]) {
+ const [group, visibility] =
+ typeof r === 'string' ? (r.endsWith('.ts') ? r.slice(0, -3) : r).split('.') : r;
+ return {
+ pkgInfo: {
+ pkgId: 'aPackage',
+ },
+ group,
+ visibility,
+ };
+ },
+ };
+ },
+ };
+});
+
+const tsTester = [
+ '@typescript-eslint/parser',
+ new RuleTester({
+ parser: require.resolve('@typescript-eslint/parser'),
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2018,
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ }),
+] as const;
+
+const babelTester = [
+ '@babel/eslint-parser',
+ new RuleTester({
+ parser: require.resolve('@babel/eslint-parser'),
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2018,
+ requireConfigFile: false,
+ babelOptions: {
+ presets: ['@kbn/babel-preset/node_preset'],
+ },
+ },
+ }),
+] as const;
+
+for (const [name, tester] of [tsTester, babelTester]) {
+ describe(name, () => {
+ tester.run('@kbn/imports/no_group_crossing_imports', NoGroupCrossingImportsRule, {
+ valid: [
+ make('observability', 'private', 'observability', 'private'),
+ make('security', 'private', 'security', 'private'),
+ make('search', 'private', 'search', 'private'),
+ make('observability', 'private', 'platform', 'shared'),
+ make('security', 'private', 'common', 'shared'),
+ make('platform', 'shared', 'platform', 'shared'),
+ make('platform', 'shared', 'platform', 'private'),
+ make('common', 'shared', 'common', 'shared'),
+ ],
+
+ invalid: [
+ {
+ ...make('observability', 'private', 'security', 'private'),
+ errors: [
+ {
+ line: 1,
+ messageId: 'ILLEGAL_IMPORT',
+ data: {
+ importerPackage: 'aPackage',
+ importerGroup: 'observability',
+ importedPackage: 'aPackage',
+ importedGroup: 'security',
+ importedVisibility: 'private',
+ sourcePath: 'observability.private.ts',
+ suggestion: formatSuggestions([
+ `Please review the dependencies in your module's manifest (kibana.jsonc).`,
+ `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
+ `Address the conflicting dependencies by refactoring the code`,
+ ]),
+ },
+ },
+ ],
+ },
+ {
+ ...make('security', 'private', 'platform', 'private'),
+ errors: [
+ {
+ line: 1,
+ messageId: 'ILLEGAL_IMPORT',
+ data: {
+ importerPackage: 'aPackage',
+ importerGroup: 'security',
+ importedPackage: 'aPackage',
+ importedGroup: 'platform',
+ importedVisibility: 'private',
+ sourcePath: 'security.private.ts',
+ suggestion: formatSuggestions([
+ `Please review the dependencies in your module's manifest (kibana.jsonc).`,
+ `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
+ `Address the conflicting dependencies by refactoring the code`,
+ ]),
+ },
+ },
+ ],
+ },
+ ],
+ });
+ });
+}
diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.ts
new file mode 100644
index 0000000000000..255973ab7460a
--- /dev/null
+++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_imports.ts
@@ -0,0 +1,77 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { dirname } from 'path';
+import type { Rule } from 'eslint';
+import type { Node } from 'estree';
+import { REPO_ROOT } from '@kbn/repo-info';
+
+import { visitAllImportStatements } from '../helpers/visit_all_import_statements';
+import { getSourcePath } from '../helpers/source';
+import { getRepoSourceClassifier } from '../helpers/repo_source_classifier';
+import { getImportResolver } from '../get_import_resolver';
+import { formatSuggestions } from '../helpers/report';
+import { isImportableFrom } from '../helpers/groups';
+
+export const NoGroupCrossingImportsRule: Rule.RuleModule = {
+ meta: {
+ docs: {
+ url: 'https://github.com/elastic/kibana/blob/main/packages/kbn-eslint-plugin-imports/README.mdx#kbnimportsno_unused_imports',
+ },
+ messages: {
+ ILLEGAL_IMPORT: `âš Illegal import statement: "{{importerPackage}}" ({{importerGroup}}) is importing "{{importedPackage}}" ({{importedGroup}}/{{importedVisibility}}). File: {{sourcePath}}\n{{suggestion}}\n`,
+ },
+ },
+ create(context) {
+ const resolver = getImportResolver(context);
+ const classifier = getRepoSourceClassifier(resolver);
+ const sourcePath = getSourcePath(context);
+ const ownDirname = dirname(sourcePath);
+ const self = classifier.classify(sourcePath);
+ const relativePath = sourcePath.replace(REPO_ROOT, '').replace(/^\//, '');
+
+ return visitAllImportStatements((req, { node }) => {
+ if (
+ req === null ||
+ // we can ignore imports using the raw-loader, they will need to be resolved but can be managed on a case by case basis
+ req.startsWith('!!raw-loader')
+ ) {
+ return;
+ }
+
+ const result = resolver.resolve(req, ownDirname);
+ if (result?.type !== 'file' || result.nodeModule) {
+ return;
+ }
+
+ const imported = classifier.classify(result.absolute);
+
+ if (!isImportableFrom(self.group, imported.group, imported.visibility)) {
+ context.report({
+ node: node as Node,
+ messageId: 'ILLEGAL_IMPORT',
+ data: {
+ importerPackage: self.pkgInfo?.pkgId ?? 'unknown',
+ importerGroup: self.group,
+ importedPackage: imported.pkgInfo?.pkgId ?? 'unknown',
+ importedGroup: imported.group,
+ importedVisibility: imported.visibility,
+ sourcePath: relativePath,
+ suggestion: formatSuggestions([
+ `Please review the dependencies in your module's manifest (kibana.jsonc).`,
+ `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
+ `Address the conflicting dependencies by refactoring the code`,
+ ]),
+ },
+ });
+ return;
+ }
+ });
+ },
+};
diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.test.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.test.ts
new file mode 100644
index 0000000000000..bf75a01b222bb
--- /dev/null
+++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.test.ts
@@ -0,0 +1,280 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { RuleTester } from 'eslint';
+import dedent from 'dedent';
+import { NoGroupCrossingManifestsRule } from './no_group_crossing_manifests';
+import { formatSuggestions } from '../helpers/report';
+import { ModuleId } from '@kbn/repo-source-classifier/src/module_id';
+import { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
+
+const makePlugin = (filename: string) => ({
+ filename,
+ code: dedent`
+ export function plugin() {
+ return new MyPlugin();
+ }
+ `,
+});
+
+const makePluginClass = (filename: string) => ({
+ filename,
+ code: dedent`
+ class MyPlugin implements Plugin {
+ setup() {
+ console.log('foo');
+ }
+ start() {
+ console.log('foo');
+ }
+ }
+ `,
+});
+
+const makeModuleByPath = (
+ path: string,
+ group: ModuleGroup,
+ visibility: ModuleVisibility,
+ pluginOverrides: any = {}
+): Record => {
+ const pluginId = path.split('/')[4];
+ const packageId = `@kbn/${pluginId}-plugin`;
+
+ return {
+ [path]: {
+ type: 'server package',
+ dirs: [],
+ repoRel: 'some/relative/path',
+ pkgInfo: {
+ pkgId: packageId,
+ pkgDir: path.split('/').slice(0, -2).join('/'),
+ rel: 'some/relative/path',
+ },
+ group,
+ visibility,
+ manifest: {
+ type: 'plugin',
+ id: packageId,
+ owner: ['@kbn/kibana-operations'],
+ plugin: {
+ id: pluginId,
+ browser: true,
+ server: true,
+ ...pluginOverrides,
+ },
+ },
+ },
+ };
+};
+
+const makeError = (line: number, ...violations: string[]) => ({
+ line,
+ messageId: 'ILLEGAL_MANIFEST_DEPENDENCY',
+ data: {
+ violations: violations.join('\n'),
+ suggestion: formatSuggestions([
+ `Please review the dependencies in your plugin's manifest (kibana.jsonc).`,
+ `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
+ `Address the conflicting dependencies by refactoring the code`,
+ ]),
+ },
+});
+
+jest.mock('../helpers/repo_source_classifier', () => {
+ const MODULES_BY_PATH: Record = {
+ ...makeModuleByPath(
+ 'path/to/search/plugins/searchPlugin1/server/index.ts',
+ 'search',
+ 'private',
+ {
+ requiredPlugins: ['searchPlugin2'], // allowed, same group
+ }
+ ),
+ ...makeModuleByPath(
+ 'path/to/search/plugins/searchPlugin2/server/index.ts',
+ 'search',
+ 'private',
+ {
+ requiredPlugins: ['securityPlugin1'], // invalid, dependency belongs to another group
+ }
+ ),
+ ...makeModuleByPath(
+ 'path/to/security/plugins/securityPlugin1/server/index.ts',
+ 'security',
+ 'private',
+ {
+ requiredPlugins: ['securityPlugin2'], // allowed, same group
+ }
+ ),
+ ...makeModuleByPath(
+ 'path/to/security/plugins/securityPlugin2/server/index.ts',
+ 'security',
+ 'private',
+ {
+ requiredPlugins: ['platformPlugin1', 'platformPlugin2', 'platformPlugin3'], // 3rd one is private!
+ }
+ ),
+ ...makeModuleByPath(
+ 'path/to/platform/shared/platformPlugin1/server/index.ts',
+ 'platform',
+ 'shared',
+ {
+ requiredPlugins: ['platformPlugin2', 'platformPlugin3', 'platformPlugin4'],
+ }
+ ),
+ ...makeModuleByPath(
+ 'path/to/platform/shared/platformPlugin2/server/index.ts',
+ 'platform',
+ 'shared'
+ ),
+ ...makeModuleByPath(
+ 'path/to/platform/private/platformPlugin3/server/index.ts',
+ 'platform',
+ 'private'
+ ),
+ ...makeModuleByPath(
+ 'path/to/platform/private/platformPlugin4/server/index.ts',
+ 'platform',
+ 'private'
+ ),
+ };
+
+ return {
+ getRepoSourceClassifier() {
+ return {
+ classify(path: string) {
+ return MODULES_BY_PATH[path];
+ },
+ };
+ },
+ };
+});
+
+jest.mock('@kbn/repo-packages', () => {
+ const original = jest.requireActual('@kbn/repo-packages');
+
+ return {
+ ...original,
+ getPluginPackagesFilter: () => () => true,
+ getPackages() {
+ return [
+ 'path/to/search/plugins/searchPlugin1/server/index.ts',
+ 'path/to/search/plugins/searchPlugin2/server/index.ts',
+ 'path/to/security/plugins/securityPlugin1/server/index.ts',
+ 'path/to/security/plugins/securityPlugin2/server/index.ts',
+ 'path/to/platform/shared/platformPlugin1/server/index.ts',
+ 'path/to/platform/shared/platformPlugin2/server/index.ts',
+ 'path/to/platform/private/platformPlugin3/server/index.ts',
+ 'path/to/platform/private/platformPlugin4/server/index.ts',
+ ].map((path) => {
+ const [, , group, , id] = path.split('/');
+ return {
+ id: `@kbn/${id}-plugin`,
+ group,
+ visibility: path.includes('platform/shared') ? 'shared' : 'private',
+ manifest: {
+ plugin: {
+ id,
+ },
+ },
+ };
+ });
+ },
+ };
+});
+
+const tsTester = [
+ '@typescript-eslint/parser',
+ new RuleTester({
+ parser: require.resolve('@typescript-eslint/parser'),
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2018,
+ ecmaFeatures: {
+ jsx: true,
+ },
+ },
+ }),
+] as const;
+
+const babelTester = [
+ '@babel/eslint-parser',
+ new RuleTester({
+ parser: require.resolve('@babel/eslint-parser'),
+ parserOptions: {
+ sourceType: 'module',
+ ecmaVersion: 2018,
+ requireConfigFile: false,
+ babelOptions: {
+ presets: ['@kbn/babel-preset/node_preset'],
+ },
+ },
+ }),
+] as const;
+
+for (const [name, tester] of [tsTester, babelTester]) {
+ describe(name, () => {
+ tester.run('@kbn/imports/no_group_crossing_manifests', NoGroupCrossingManifestsRule, {
+ valid: [
+ makePlugin('path/to/search/plugins/searchPlugin1/server/index.ts'),
+ makePlugin('path/to/security/plugins/securityPlugin1/server/index.ts'),
+ makePlugin('path/to/platform/shared/platformPlugin1/server/index.ts'),
+ makePluginClass('path/to/search/plugins/searchPlugin1/server/index.ts'),
+ makePluginClass('path/to/security/plugins/securityPlugin1/server/index.ts'),
+ makePluginClass('path/to/platform/shared/platformPlugin1/server/index.ts'),
+ ],
+ invalid: [
+ {
+ ...makePlugin('path/to/search/plugins/searchPlugin2/server/index.ts'),
+ errors: [
+ makeError(
+ 1,
+ `âš Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc`
+ ),
+ ],
+ },
+ {
+ ...makePlugin('path/to/security/plugins/securityPlugin2/server/index.ts'),
+ errors: [
+ makeError(
+ 1,
+ `âš Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc`
+ ),
+ ],
+ },
+ {
+ ...makePluginClass('path/to/search/plugins/searchPlugin2/server/index.ts'),
+ errors: [
+ makeError(
+ 2,
+ `âš Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc`
+ ),
+ makeError(
+ 5,
+ `âš Illegal dependency on manifest: Plugin "searchPlugin2" (package: "@kbn/searchPlugin2-plugin"; group: "search") depends on "securityPlugin1" (package: "@kbn/securityPlugin1-plugin"; group: security/private). File: path/to/search/plugins/searchPlugin2/kibana.jsonc`
+ ),
+ ],
+ },
+ {
+ ...makePluginClass('path/to/security/plugins/securityPlugin2/server/index.ts'),
+ errors: [
+ makeError(
+ 2,
+ `âš Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc`
+ ),
+ makeError(
+ 5,
+ `âš Illegal dependency on manifest: Plugin "securityPlugin2" (package: "@kbn/securityPlugin2-plugin"; group: "security") depends on "platformPlugin3" (package: "@kbn/platformPlugin3-plugin"; group: platform/private). File: path/to/security/plugins/securityPlugin2/kibana.jsonc`
+ ),
+ ],
+ },
+ ],
+ });
+ });
+}
diff --git a/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.ts b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.ts
new file mode 100644
index 0000000000000..e68f7217905a5
--- /dev/null
+++ b/packages/kbn-eslint-plugin-imports/src/rules/no_group_crossing_manifests.ts
@@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { join } from 'path';
+import { TSESTree } from '@typescript-eslint/typescript-estree';
+import type { Rule } from 'eslint';
+import type { Node } from 'estree';
+import { getPackages, getPluginPackagesFilter } from '@kbn/repo-packages';
+import { REPO_ROOT } from '@kbn/repo-info';
+import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
+import { getSourcePath } from '../helpers/source';
+import { getImportResolver } from '../get_import_resolver';
+import { getRepoSourceClassifier } from '../helpers/repo_source_classifier';
+import { isImportableFrom } from '../helpers/groups';
+import { formatSuggestions } from '../helpers/report';
+
+const NODE_TYPES = TSESTree.AST_NODE_TYPES;
+
+interface PluginInfo {
+ id: string;
+ pluginId: string;
+ group: ModuleGroup;
+ visibility: ModuleVisibility;
+}
+
+export const NoGroupCrossingManifestsRule: Rule.RuleModule = {
+ meta: {
+ docs: {
+ url: 'https://github.com/elastic/kibana/blob/main/packages/kbn-eslint-plugin-imports/README.mdx#kbnimportsno_unused_imports',
+ },
+ messages: {
+ ILLEGAL_MANIFEST_DEPENDENCY: `{{violations}}\n{{suggestion}}`,
+ },
+ },
+ create(context) {
+ const sourcePath = getSourcePath(context);
+ let manifestPath: string;
+ const resolver = getImportResolver(context);
+ const classifier = getRepoSourceClassifier(resolver);
+ const moduleId = classifier.classify(sourcePath);
+ const offendingDependencies: PluginInfo[] = [];
+ let currentPlugin: PluginInfo;
+
+ if (moduleId.manifest?.type === 'plugin') {
+ manifestPath = join(moduleId.pkgInfo!.pkgDir, 'kibana.jsonc')
+ .replace(REPO_ROOT, '')
+ .replace(/^\//, '');
+ currentPlugin = {
+ id: moduleId.pkgInfo!.pkgId,
+ pluginId: moduleId.manifest.plugin.id,
+ group: moduleId.group,
+ visibility: moduleId.visibility,
+ };
+
+ const allPlugins = getPackages(REPO_ROOT).filter(getPluginPackagesFilter());
+ const currentPluginInfo = moduleId.manifest!.plugin;
+ // check all the dependencies in the manifest, looking for plugin violations
+ [
+ ...(currentPluginInfo.requiredPlugins ?? []),
+ ...(currentPluginInfo.requiredBundles ?? []),
+ ...(currentPluginInfo.optionalPlugins ?? []),
+ ...(currentPluginInfo.runtimePluginDependencies ?? []),
+ ].forEach((pluginId) => {
+ const dependency = allPlugins.find(({ manifest }) => manifest.plugin.id === pluginId);
+ if (dependency) {
+ // at this point, we know the dependency is a plugin
+ const { id, group, visibility } = dependency;
+ if (!isImportableFrom(moduleId.group, group, visibility)) {
+ offendingDependencies.push({ id, pluginId, group, visibility });
+ }
+ }
+ });
+ }
+
+ return {
+ FunctionDeclaration(node) {
+ // complain in exported plugin() function
+ if (
+ currentPlugin &&
+ offendingDependencies.length &&
+ node.id?.name === 'plugin' &&
+ node.parent.type === NODE_TYPES.ExportNamedDeclaration
+ ) {
+ reportViolation({
+ context,
+ node,
+ currentPlugin,
+ manifestPath,
+ offendingDependencies,
+ });
+ }
+ },
+ MethodDefinition(node) {
+ // complain in setup() and start() hooks
+ if (
+ offendingDependencies.length &&
+ node.key.type === NODE_TYPES.Identifier &&
+ (node.key.name === 'setup' || node.key.name === 'start') &&
+ node.kind === 'method' &&
+ node.parent.parent.type === NODE_TYPES.ClassDeclaration &&
+ (node.parent.parent.id?.name.includes('Plugin') ||
+ (node.parent.parent as TSESTree.ClassDeclaration).implements?.find(
+ (value) =>
+ value.expression.type === NODE_TYPES.Identifier &&
+ value.expression.name === 'Plugin'
+ ))
+ ) {
+ reportViolation({
+ context,
+ node,
+ currentPlugin,
+ manifestPath,
+ offendingDependencies,
+ });
+ }
+ },
+ };
+ },
+};
+
+interface ReportViolationParams {
+ context: Rule.RuleContext;
+ node: Node;
+ currentPlugin: PluginInfo;
+ offendingDependencies: PluginInfo[];
+ manifestPath: string;
+}
+
+const reportViolation = ({
+ context,
+ node,
+ currentPlugin,
+ offendingDependencies,
+ manifestPath,
+}: ReportViolationParams) =>
+ context.report({
+ node,
+ messageId: 'ILLEGAL_MANIFEST_DEPENDENCY',
+ data: {
+ violations: [
+ ...offendingDependencies.map(
+ ({ id, pluginId, group, visibility }) =>
+ `âš Illegal dependency on manifest: Plugin "${currentPlugin.pluginId}" (package: "${currentPlugin.id}"; group: "${currentPlugin.group}") depends on "${pluginId}" (package: "${id}"; group: ${group}/${visibility}). File: ${manifestPath}`
+ ),
+ ].join('\n'),
+ suggestion: formatSuggestions([
+ `Please review the dependencies in your plugin's manifest (kibana.jsonc).`,
+ `Relocate this module to a different group, and/or make sure it has the right 'visibility'.`,
+ `Address the conflicting dependencies by refactoring the code`,
+ ]),
+ },
+ });
diff --git a/packages/kbn-eslint-plugin-imports/tsconfig.json b/packages/kbn-eslint-plugin-imports/tsconfig.json
index 087d77fbfe437..b0ab9182171c3 100644
--- a/packages/kbn-eslint-plugin-imports/tsconfig.json
+++ b/packages/kbn-eslint-plugin-imports/tsconfig.json
@@ -14,6 +14,7 @@
"@kbn/import-resolver",
"@kbn/repo-source-classifier",
"@kbn/repo-info",
+ "@kbn/repo-packages",
],
"exclude": [
"target/**/*",
diff --git a/packages/kbn-generate/src/commands/codeowners_command.ts b/packages/kbn-generate/src/commands/codeowners_command.ts
index 79f7025b99a02..a86b4250d6850 100644
--- a/packages/kbn-generate/src/commands/codeowners_command.ts
+++ b/packages/kbn-generate/src/commands/codeowners_command.ts
@@ -63,7 +63,11 @@ export const CodeownersCommand: GenerateCommand = {
}
const newCodeowners = `${GENERATED_START}${pkgs
- .map((pkg) => `${pkg.normalizedRepoRelativeDir} ${pkg.manifest.owner.join(' ')}`)
+ .map(
+ (pkg) =>
+ pkg.normalizedRepoRelativeDir +
+ (pkg.manifest.owner.length ? ' ' + pkg.manifest.owner.join(' ') : '')
+ )
.join('\n')}${GENERATED_END}${content}${ULTIMATE_PRIORITY_RULES}`;
if (newCodeowners === oldCodeowners) {
diff --git a/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts b/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts
index 441df3948632b..30682d763e0b0 100644
--- a/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts
+++ b/packages/kbn-kibana-manifest-schema/src/kibana_json_v2_schema.ts
@@ -48,6 +48,20 @@ export const MANIFEST_V2: JSONSchema = {
For additional codeowners, the value can be an array of user/team names.
`,
},
+ group: {
+ enum: ['common', 'platform', 'observability', 'security', 'search'],
+ description: desc`
+ Specifies the group to which this module pertains.
+ `,
+ default: 'common',
+ },
+ visibility: {
+ enum: ['private', 'shared'],
+ description: desc`
+ Specifies the visibility of this module, i.e. whether it can be accessed by everybody or only modules in the same group
+ `,
+ default: 'shared',
+ },
devOnly: {
type: 'boolean',
description: desc`
diff --git a/packages/kbn-manifest/README.md b/packages/kbn-manifest/README.md
new file mode 100644
index 0000000000000..a7dc2054252dc
--- /dev/null
+++ b/packages/kbn-manifest/README.md
@@ -0,0 +1,30 @@
+# @kbn/manifest
+
+This package contains a CLI to list `kibana.jsonc` manifests and also to mass update their properties.
+
+## Usage
+
+To list all `kibana.jsonc` manifests, run the following command from the root of the Kibana repo:
+
+```sh
+node scripts/manifest --list all
+```
+
+To print a manifest by packageId or by pluginId, run the following command from the root of the Kibana repo:
+
+```sh
+node scripts/manifest --package @kbn/package_name
+node scripts/manifest --plugin pluginId
+```
+
+To update properties in one or more manifest files, run the following command from the root of the Kibana repo:
+
+```sh
+node scripts/manifest \
+--package @kbn/package_1 \
+--package @kbn/package_2 \
+# ...
+--package @kbn/package_N \
+--set path.to.property1=value \
+--set property2=value
+```
diff --git a/packages/kbn-manifest/index.ts b/packages/kbn-manifest/index.ts
new file mode 100644
index 0000000000000..5fc4727a1a72d
--- /dev/null
+++ b/packages/kbn-manifest/index.ts
@@ -0,0 +1,46 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { run } from '@kbn/dev-cli-runner';
+import { listManifestFiles, printManifest, updateManifest } from './manifest';
+
+/**
+ * A CLI to manipulate Kibana package manifest files
+ */
+export const runKbnManifestCli = () => {
+ run(
+ async ({ log, flags }) => {
+ if (flags.list === 'all') {
+ listManifestFiles(flags, log);
+ } else {
+ if (!flags.package && !flags.plugin) {
+ throw new Error('You must specify the identifer of the --package or --plugin to update.');
+ }
+ await updateManifest(flags, log);
+ await printManifest(flags, log);
+ }
+ },
+ {
+ log: {
+ defaultLevel: 'info',
+ },
+ flags: {
+ string: ['list', 'package', 'plugin', 'set', 'unset'],
+ help: `
+ Usage: node scripts/manifest --package --set group=platform --set visibility=private
+ --list all List all the manifests
+ --package [packageId] Select a package to update.
+ --plugin [pluginId] Select a plugin to update.
+ --set [property] [value] Set the desired "[property]": "[value]"
+ --unset [property] Removes the desired "[property]: value" from the manifest
+ `,
+ },
+ }
+ );
+};
diff --git a/packages/kbn-manifest/jest.config.js b/packages/kbn-manifest/jest.config.js
new file mode 100644
index 0000000000000..ed8288d9fb712
--- /dev/null
+++ b/packages/kbn-manifest/jest.config.js
@@ -0,0 +1,14 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+module.exports = {
+ preset: '@kbn/test/jest_node',
+ rootDir: '../..',
+ roots: ['/packages/kbn-manifest'],
+};
diff --git a/packages/kbn-manifest/kibana.jsonc b/packages/kbn-manifest/kibana.jsonc
new file mode 100644
index 0000000000000..27f2d95e65501
--- /dev/null
+++ b/packages/kbn-manifest/kibana.jsonc
@@ -0,0 +1,5 @@
+{
+ "type": "shared-server",
+ "id": "@kbn/manifest",
+ "owner": "@elastic/kibana-core"
+}
diff --git a/packages/kbn-manifest/manifest.ts b/packages/kbn-manifest/manifest.ts
new file mode 100644
index 0000000000000..a839dba7b4077
--- /dev/null
+++ b/packages/kbn-manifest/manifest.ts
@@ -0,0 +1,113 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import { join } from 'path';
+import { writeFile } from 'fs/promises';
+import { flatMap, unset } from 'lodash';
+import { set } from '@kbn/safer-lodash-set';
+import type { ToolingLog } from '@kbn/tooling-log';
+import type { Flags } from '@kbn/dev-cli-runner';
+import { type Package, getPackages } from '@kbn/repo-packages';
+import { REPO_ROOT } from '@kbn/repo-info';
+
+const MANIFEST_FILE = 'kibana.jsonc';
+
+const getKibanaJsonc = (flags: Flags, log: ToolingLog): Package[] => {
+ const modules = getPackages(REPO_ROOT);
+
+ let packageIds: string[] = [];
+ let pluginIds: string[] = [];
+
+ if (typeof flags.package === 'string') {
+ packageIds = [flags.package].filter(Boolean);
+ } else if (Array.isArray(flags.package)) {
+ packageIds = [...flags.package].filter(Boolean);
+ }
+
+ if (typeof flags.plugin === 'string') {
+ pluginIds = [flags.plugin].filter(Boolean);
+ } else if (Array.isArray(flags.plugin)) {
+ pluginIds = [...flags.plugin].filter(Boolean);
+ }
+
+ return modules.filter(
+ (pkg) =>
+ packageIds.includes(pkg.id) || (pkg.isPlugin() && pluginIds.includes(pkg.manifest.plugin.id))
+ );
+};
+
+export const listManifestFiles = (flags: Flags, log: ToolingLog) => {
+ const modules = getPackages(REPO_ROOT);
+ modules
+ .filter((module) => module.manifest.type === 'plugin')
+ .forEach((module) => {
+ log.info(join(module.directory, MANIFEST_FILE), module.id);
+ });
+};
+
+export const printManifest = (flags: Flags, log: ToolingLog) => {
+ const kibanaJsoncs = getKibanaJsonc(flags, log);
+ kibanaJsoncs.forEach((kibanaJsonc) => {
+ const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE);
+ log.info('\n\nShowing manifest: ', manifestPath);
+ log.info(JSON.stringify(kibanaJsonc, null, 2));
+ });
+};
+
+export const updateManifest = async (flags: Flags, log: ToolingLog) => {
+ let toSet: string[] = [];
+ let toUnset: string[] = [];
+
+ if (typeof flags.set === 'string') {
+ toSet = [flags.set].filter(Boolean);
+ } else if (Array.isArray(flags.set)) {
+ toSet = [...flags.set].filter(Boolean);
+ }
+
+ if (typeof flags.unset === 'string') {
+ toUnset = [flags.unset].filter(Boolean);
+ } else if (Array.isArray(flags.unset)) {
+ toUnset = [...flags.unset].filter(Boolean);
+ }
+
+ if (!toSet.length && !toUnset.length) {
+ // no need to update anything
+ return;
+ }
+
+ const kibanaJsoncs = getKibanaJsonc(flags, log);
+
+ for (let i = 0; i < kibanaJsoncs.length; ++i) {
+ const kibanaJsonc = kibanaJsoncs[i];
+
+ if (kibanaJsonc?.manifest) {
+ const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE);
+ log.info('Updating manifest: ', manifestPath);
+ toSet.forEach((propValue) => {
+ const [prop, value] = propValue.split('=');
+ log.info(`Setting "${prop}": "${value}"`);
+ set(kibanaJsonc.manifest, prop, value);
+ });
+
+ toUnset.forEach((prop) => {
+ log.info(`Removing "${prop}"`);
+ unset(kibanaJsonc.manifest, prop);
+ });
+
+ sanitiseManifest(kibanaJsonc);
+
+ await writeFile(manifestPath, JSON.stringify(kibanaJsonc.manifest, null, 2));
+ log.info('DONE');
+ }
+ }
+};
+
+const sanitiseManifest = (kibanaJsonc: Package) => {
+ kibanaJsonc.manifest.owner = flatMap(kibanaJsonc.manifest.owner.map((owner) => owner.split(' ')));
+};
diff --git a/packages/kbn-manifest/package.json b/packages/kbn-manifest/package.json
new file mode 100644
index 0000000000000..52304cc4c1e21
--- /dev/null
+++ b/packages/kbn-manifest/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "@kbn/manifest",
+ "private": true,
+ "version": "1.0.0",
+ "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0"
+}
diff --git a/packages/kbn-manifest/tsconfig.json b/packages/kbn-manifest/tsconfig.json
new file mode 100644
index 0000000000000..1ee41aafca1ee
--- /dev/null
+++ b/packages/kbn-manifest/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "outDir": "target/types",
+ "types": [
+ "jest",
+ "node"
+ ]
+ },
+ "include": [
+ "**/*.ts",
+ ],
+ "exclude": [
+ "target/**/*"
+ ],
+ "kbn_references": [
+ "@kbn/dev-cli-runner",
+ "@kbn/repo-info",
+ "@kbn/repo-packages",
+ "@kbn/safer-lodash-set",
+ "@kbn/tooling-log",
+ ]
+}
diff --git a/packages/kbn-repo-info/types.ts b/packages/kbn-repo-info/types.ts
index a4776c28760a2..338881e878fdc 100644
--- a/packages/kbn-repo-info/types.ts
+++ b/packages/kbn-repo-info/types.ts
@@ -7,6 +7,9 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+export type ModuleGroup = 'platform' | 'observability' | 'search' | 'security' | 'common';
+export type ModuleVisibility = 'private' | 'shared';
+
export interface KibanaPackageJson {
name: string;
version: string;
@@ -27,4 +30,6 @@ export interface KibanaPackageJson {
[name: string]: string | undefined;
};
[key: string]: unknown;
+ group?: ModuleGroup;
+ visibility?: ModuleVisibility;
}
diff --git a/packages/kbn-repo-packages/modern/package.js b/packages/kbn-repo-packages/modern/package.js
index 1c44cd0cf86d9..3ec33a69e841a 100644
--- a/packages/kbn-repo-packages/modern/package.js
+++ b/packages/kbn-repo-packages/modern/package.js
@@ -116,6 +116,22 @@ class Package {
* @readonly
*/
this.id = manifest.id;
+
+ const { group, visibility } = this.determineGroupAndVisibility();
+
+ /**
+ * the group to which this package belongs
+ * @type {import('@kbn/repo-info/types').ModuleGroup}
+ * @readonly
+ */
+
+ this.group = group;
+ /**
+ * the visibility of this package, i.e. whether it can be accessed by everybody or only modules in the same group
+ * @type {import('@kbn/repo-info/types').ModuleVisibility}
+ * @readonly
+ */
+ this.visibility = visibility;
}
/**
@@ -140,6 +156,24 @@ class Package {
return this.manifest.type === 'plugin';
}
+ /**
+ * Returns the group to which this package belongs
+ * @readonly
+ * @returns {import('@kbn/repo-info/types').ModuleGroup}
+ */
+ getGroup() {
+ return this.group;
+ }
+
+ /**
+ * Returns the package visibility, i.e. whether it can be accessed by everybody or only packages in the same group
+ * @readonly
+ * @returns {import('@kbn/repo-info/types').ModuleVisibility}
+ */
+ getVisibility() {
+ return this.visibility;
+ }
+
/**
* Returns true if the package represents some type of plugin
* @returns {import('./types').PluginCategoryInfo}
@@ -158,6 +192,7 @@ class Package {
const oss = !dir.startsWith('x-pack/');
const example = dir.startsWith('examples/') || dir.startsWith('x-pack/examples/');
const testPlugin = dir.startsWith('test/') || dir.startsWith('x-pack/test/');
+
return {
oss,
example,
@@ -165,6 +200,40 @@ class Package {
};
}
+ determineGroupAndVisibility() {
+ const dir = this.normalizedRepoRelativeDir;
+
+ /** @type {import('@kbn/repo-info/types').ModuleGroup} */
+ let group = 'common';
+ /** @type {import('@kbn/repo-info/types').ModuleVisibility} */
+ let visibility = 'shared';
+
+ if (dir.startsWith('src/platform/') || dir.startsWith('x-pack/platform/')) {
+ group = 'platform';
+ visibility =
+ /src\/platform\/[^\/]+\/shared/.test(dir) || /x-pack\/platform\/[^\/]+\/shared/.test(dir)
+ ? 'shared'
+ : 'private';
+ } else if (dir.startsWith('x-pack/solutions/search/')) {
+ group = 'search';
+ visibility = 'private';
+ } else if (dir.startsWith('x-pack/solutions/security/')) {
+ group = 'security';
+ visibility = 'private';
+ } else if (dir.startsWith('x-pack/solutions/observability/')) {
+ group = 'observability';
+ visibility = 'private';
+ } else {
+ group = this.manifest.group ?? 'common';
+ // if the group is 'private-only', enforce it
+ visibility = ['search', 'security', 'observability'].includes(group)
+ ? 'private'
+ : this.manifest.visibility ?? 'shared';
+ }
+
+ return { group, visibility };
+ }
+
/**
* Custom inspect handler so that logging variables in scripts/generate doesn't
* print all the BUILD.bazel files
diff --git a/packages/kbn-repo-packages/modern/parse_package_manifest.js b/packages/kbn-repo-packages/modern/parse_package_manifest.js
index 40a6f7bf1059b..46004983848bb 100644
--- a/packages/kbn-repo-packages/modern/parse_package_manifest.js
+++ b/packages/kbn-repo-packages/modern/parse_package_manifest.js
@@ -225,16 +225,20 @@ function validatePackageManifest(parsed, repoRoot, path) {
type,
id,
owner,
+ group,
+ visibility,
devOnly,
- plugin,
- sharedBrowserBundle,
build,
description,
serviceFolders,
...extra
- } = parsed;
+ } = /** @type {import('./types').PackageManifestBaseFields} */ (/** @type {unknown} */ (parsed));
- const extraKeys = Object.keys(extra);
+ const { plugin, sharedBrowserBundle } = parsed;
+
+ const extraKeys = Object.keys(extra).filter(
+ (key) => !['plugin', 'sharedBrowserBundle'].includes(key)
+ );
if (extraKeys.length) {
throw new Error(`unexpected keys in package manifest [${extraKeys.join(', ')}]`);
}
@@ -258,6 +262,25 @@ function validatePackageManifest(parsed, repoRoot, path) {
);
}
+ if (
+ group !== undefined &&
+ (!isSomeString(group) ||
+ !['platform', 'search', 'security', 'observability', 'common'].includes(group))
+ ) {
+ throw err(
+ `plugin.group`,
+ group,
+ `must have a valid value ("platform" | "search" | "security" | "observability" | "common")`
+ );
+ }
+
+ if (
+ visibility !== undefined &&
+ (!isSomeString(visibility) || !['private', 'shared'].includes(visibility))
+ ) {
+ throw err(`plugin.visibility`, visibility, `must have a valid value ("private" | "shared")`);
+ }
+
if (devOnly !== undefined && typeof devOnly !== 'boolean') {
throw err(`devOnly`, devOnly, `must be a boolean when defined`);
}
@@ -273,6 +296,8 @@ function validatePackageManifest(parsed, repoRoot, path) {
const base = {
id,
owner: Array.isArray(owner) ? owner : [owner],
+ group,
+ visibility,
devOnly,
build: validatePackageManifestBuild(build),
description,
diff --git a/packages/kbn-repo-packages/modern/types.ts b/packages/kbn-repo-packages/modern/types.ts
index 41250de7c6346..c883e33d82497 100644
--- a/packages/kbn-repo-packages/modern/types.ts
+++ b/packages/kbn-repo-packages/modern/types.ts
@@ -7,6 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
import type { Package } from './package';
import type { PLUGIN_CATEGORY } from './plugin_category_info';
@@ -44,7 +45,7 @@ export type KibanaPackageType =
| 'functional-tests'
| 'test-helper';
-interface PackageManifestBaseFields {
+export interface PackageManifestBaseFields {
/**
* The type of this package. Package types define how a package can and should
* be used/built. Some package types also change the way that packages are
@@ -91,6 +92,14 @@ interface PackageManifestBaseFields {
* @deprecated
*/
serviceFolders?: string[];
+ /**
+ * Specifies the group to which this package belongs
+ */
+ group?: ModuleGroup;
+ /**
+ * Specifies the package visibility, i.e. whether it can be accessed by everybody or only packages in the same group
+ */
+ visibility?: ModuleVisibility;
}
export interface PluginPackageManifest extends PackageManifestBaseFields {
diff --git a/packages/kbn-repo-packages/tsconfig.json b/packages/kbn-repo-packages/tsconfig.json
index 19c7e8d59f651..be62cc1a4c90b 100644
--- a/packages/kbn-repo-packages/tsconfig.json
+++ b/packages/kbn-repo-packages/tsconfig.json
@@ -14,5 +14,8 @@
],
"exclude": [
"target/**/*",
+ ],
+ "kbn_references": [
+ "@kbn/repo-info",
]
}
diff --git a/packages/kbn-repo-source-classifier/src/group.ts b/packages/kbn-repo-source-classifier/src/group.ts
new file mode 100644
index 0000000000000..8103d5c82c590
--- /dev/null
+++ b/packages/kbn-repo-source-classifier/src/group.ts
@@ -0,0 +1,63 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
+
+interface ModuleAttrs {
+ group: ModuleGroup;
+ visibility: ModuleVisibility;
+}
+
+const DEFAULT_MODULE_ATTRS: ModuleAttrs = {
+ group: 'common',
+ visibility: 'shared',
+};
+
+const MODULE_GROUPING_BY_PATH: Record = {
+ 'src/platform/plugins/shared': {
+ group: 'platform',
+ visibility: 'shared',
+ },
+ 'src/platform/plugins/internal': {
+ group: 'platform',
+ visibility: 'private',
+ },
+ 'x-pack/platform/plugins/shared': {
+ group: 'platform',
+ visibility: 'shared',
+ },
+ 'x-pack/platform/plugins/internal': {
+ group: 'platform',
+ visibility: 'private',
+ },
+ 'x-pack/solutions/observability/plugins': {
+ group: 'observability',
+ visibility: 'private',
+ },
+ 'x-pack/solutions/security/plugins': {
+ group: 'security',
+ visibility: 'private',
+ },
+ 'x-pack/solutions/search/plugins': {
+ group: 'search',
+ visibility: 'private',
+ },
+};
+
+/**
+ * Determine a plugin's grouping information based on the path where it is defined
+ * @param packageRelativePath the path in the repo where the package is located
+ * @returns The grouping information that corresponds to the given path
+ */
+export function inferGroupAttrsFromPath(packageRelativePath: string): ModuleAttrs {
+ const grouping = Object.entries(MODULE_GROUPING_BY_PATH).find(([chunk]) =>
+ packageRelativePath.startsWith(chunk)
+ )?.[1];
+ return grouping ?? DEFAULT_MODULE_ATTRS;
+}
diff --git a/packages/kbn-repo-source-classifier/src/module_id.ts b/packages/kbn-repo-source-classifier/src/module_id.ts
index 6af8ece2438fa..284ffe26de0db 100644
--- a/packages/kbn-repo-source-classifier/src/module_id.ts
+++ b/packages/kbn-repo-source-classifier/src/module_id.ts
@@ -7,16 +7,24 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { ModuleType } from './module_type';
-import { PkgInfo } from './pkg_info';
+import type { KibanaPackageManifest } from '@kbn/repo-packages';
+import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
+import type { ModuleType } from './module_type';
+import type { PkgInfo } from './pkg_info';
export interface ModuleId {
/** Type of the module */
type: ModuleType;
+ /** Specifies the group to which this module belongs */
+ group: ModuleGroup;
+ /** Specifies the module visibility, i.e. whether it can be accessed by everybody or only modules in the same group */
+ visibility: ModuleVisibility;
/** repo relative path to the module's source file */
repoRel: string;
/** info about the package the source file is within, in the case the file is found within a package */
pkgInfo?: PkgInfo;
+ /** The type of package, as described in the manifest */
+ manifest?: KibanaPackageManifest;
/** path segments of the dirname of this */
dirs: string[];
}
diff --git a/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts b/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts
index 470dd3c424421..c0ab29f659ebd 100644
--- a/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts
+++ b/packages/kbn-repo-source-classifier/src/repo_source_classifier.ts
@@ -7,11 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { ImportResolver } from '@kbn/import-resolver';
-import { ModuleId } from './module_id';
-import { ModuleType } from './module_type';
+import type { ImportResolver } from '@kbn/import-resolver';
+import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
+import type { KibanaPackageManifest } from '@kbn/repo-packages/modern/types';
+import type { ModuleId } from './module_id';
+import type { ModuleType } from './module_type';
import { RANDOM_TEST_FILE_NAMES, TEST_DIR, TEST_TAG } from './config';
import { RepoPath } from './repo_path';
+import { inferGroupAttrsFromPath } from './group';
const STATIC_EXTS = new Set(
'json|woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg|html|md|txt|tmpl|xml'
@@ -231,7 +234,43 @@ export class RepoSourceClassifier {
return 'common package';
}
- classify(absolute: string) {
+ private getManifest(path: RepoPath): KibanaPackageManifest | undefined {
+ const pkgInfo = path.getPkgInfo();
+ return pkgInfo?.pkgId ? this.resolver.getPkgManifest(pkgInfo!.pkgId) : undefined;
+ }
+ /**
+ * Determine the "group" of a file
+ */
+ private getGroup(path: RepoPath): ModuleGroup {
+ const attrs = inferGroupAttrsFromPath(path.getRepoRel());
+ const manifest = this.getManifest(path);
+
+ if (attrs.group !== 'common') {
+ // this package has been moved to a 'group-specific' folder, the group is determined by its location
+ return attrs.group;
+ } else {
+ // the package is still in its original location, allow Manifest to dictate its group
+ return manifest?.group ?? 'common';
+ }
+ }
+
+ /**
+ * Determine the "visibility" of a file
+ */
+ private getVisibility(path: RepoPath): ModuleVisibility {
+ const attrs = inferGroupAttrsFromPath(path.getRepoRel());
+ const manifest = this.getManifest(path);
+
+ if (attrs.group !== 'common') {
+ // this package has been moved to a 'group-specific' folder, the visibility is determined by its location
+ return attrs.visibility;
+ } else {
+ // the package is still in its original location, allow Manifest to dictate its visibility
+ return manifest?.visibility ?? 'shared';
+ }
+ }
+
+ classify(absolute: string): ModuleId {
const path = this.getRepoPath(absolute);
const cached = this.ids.get(path);
@@ -241,8 +280,12 @@ export class RepoSourceClassifier {
const id: ModuleId = {
type: this.getType(path),
+ group: this.getGroup(path),
+ visibility: this.getVisibility(path),
repoRel: path.getRepoRel(),
pkgInfo: path.getPkgInfo() ?? undefined,
+ manifest:
+ (path.getPkgInfo() && this.resolver.getPkgManifest(path.getPkgInfo()!.pkgId)) ?? undefined,
dirs: path.getSegs(),
};
this.ids.set(path, id);
diff --git a/packages/kbn-repo-source-classifier/tsconfig.json b/packages/kbn-repo-source-classifier/tsconfig.json
index f41dffcd32f06..418b114eebafa 100644
--- a/packages/kbn-repo-source-classifier/tsconfig.json
+++ b/packages/kbn-repo-source-classifier/tsconfig.json
@@ -13,6 +13,7 @@
"kbn_references": [
"@kbn/import-resolver",
"@kbn/repo-info",
+ "@kbn/repo-packages",
],
"exclude": [
"target/**/*",
diff --git a/scripts/manifest.js b/scripts/manifest.js
new file mode 100644
index 0000000000000..f9da9c3d174bd
--- /dev/null
+++ b/scripts/manifest.js
@@ -0,0 +1,11 @@
+/*
+ * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
+ * Public License v 1"; you may not use this file except in compliance with, at
+ * your election, the "Elastic License 2.0", the "GNU Affero General Public
+ * License v3.0 only", or the "Server Side Public License, v 1".
+ */
+
+require('../src/setup_node_env');
+require('@kbn/manifest').runKbnManifestCli();
diff --git a/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx b/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx
index 8cf45be4f09e5..3171c5e61e629 100644
--- a/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx
+++ b/src/plugins/discover/public/components/data_types/logs/service_name_cell.test.tsx
@@ -7,15 +7,46 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
+import React from 'react';
import { buildDataTableRecord, DataTableRecord } from '@kbn/discover-utils';
import { dataViewMock } from '@kbn/discover-utils/src/__mocks__';
import { fieldFormatsMock } from '@kbn/field-formats-plugin/common/mocks';
import { render, screen } from '@testing-library/react';
-import React from 'react';
+import { DataGridDensity, ROWS_HEIGHT_OPTIONS } from '@kbn/unified-data-table';
import { getServiceNameCell } from './service_name_cell';
+import { CellRenderersExtensionParams } from '../../../context_awareness';
+
+const core = {
+ application: {
+ capabilities: {
+ apm: {
+ show: true,
+ },
+ },
+ },
+ uiSettings: {
+ get: () => true,
+ },
+};
+
+jest.mock('../../../hooks/use_discover_services', () => {
+ const originalModule = jest.requireActual('../../../hooks/use_discover_services');
+ return {
+ ...originalModule,
+ useDiscoverServices: () => ({ core, share: {} }),
+ };
+});
const renderCell = (serviceNameField: string, record: DataTableRecord) => {
- const ServiceNameCell = getServiceNameCell(serviceNameField);
+ const cellRenderersExtensionParamsMock: CellRenderersExtensionParams = {
+ actions: {
+ addFilter: jest.fn(),
+ },
+ dataView: dataViewMock,
+ density: DataGridDensity.COMPACT,
+ rowHeight: ROWS_HEIGHT_OPTIONS.single,
+ };
+ const ServiceNameCell = getServiceNameCell(serviceNameField, cellRenderersExtensionParamsMock);
render(
{
dataViewMock
);
renderCell('service.name', record);
- expect(screen.getByTestId('serviceNameCell-nodejs')).toBeInTheDocument();
- });
-
- it('renders default icon with unknwon test subject if agent name is missing', () => {
- const record = buildDataTableRecord(
- { fields: { 'service.name': 'test-service' } },
- dataViewMock
- );
- renderCell('service.name', record);
- expect(screen.getByTestId('serviceNameCell-unknown')).toBeInTheDocument();
+ expect(screen.getByTestId('dataTableCellActionsPopover_service.name')).toBeInTheDocument();
});
- it('does not render if service name is missing', () => {
+ it('does render empty div if service name is missing', () => {
const record = buildDataTableRecord({ fields: { 'agent.name': 'nodejs' } }, dataViewMock);
renderCell('service.name', record);
- expect(screen.queryByTestId('serviceNameCell-nodejs')).not.toBeInTheDocument();
- expect(screen.queryByTestId('serviceNameCell-unknown')).not.toBeInTheDocument();
+ expect(screen.queryByTestId('serviceNameCell-empty')).toBeInTheDocument();
});
});
diff --git a/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx b/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx
index 39d112de5258e..cd94cd609dc69 100644
--- a/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx
+++ b/src/plugins/discover/public/components/data_types/logs/service_name_cell.tsx
@@ -7,19 +7,27 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
-import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
+import React from 'react';
+import { EuiToolTip } from '@elastic/eui';
import type { AgentName } from '@kbn/elastic-agent-utils';
import { dynamic } from '@kbn/shared-ux-utility';
import type { DataGridCellValueElementProps } from '@kbn/unified-data-table';
-import React from 'react';
+import { css } from '@emotion/react';
import { getFieldValue } from '@kbn/discover-utils';
+import { euiThemeVars } from '@kbn/ui-theme';
+import { CellRenderersExtensionParams } from '../../../context_awareness';
import { AGENT_NAME_FIELD } from '../../../../common/data_types/logs/constants';
+import { ServiceNameBadgeWithActions } from './service_name_badge_with_actions';
-const dataTestSubj = 'serviceNameCell';
const AgentIcon = dynamic(() => import('@kbn/custom-icons/src/components/agent_icon'));
+const dataTestSubj = 'serviceNameCell';
+const agentIconStyle = css`
+ margin-right: ${euiThemeVars.euiSizeXS};
+`;
export const getServiceNameCell =
- (serviceNameField: string) => (props: DataGridCellValueElementProps) => {
+ (serviceNameField: string, { actions }: CellRenderersExtensionParams) =>
+ (props: DataGridCellValueElementProps) => {
const serviceNameValue = getFieldValue(props.row, serviceNameField) as string;
const agentName = getFieldValue(props.row, AGENT_NAME_FIELD) as AgentName;
@@ -27,19 +35,18 @@ export const getServiceNameCell =
return -;
}
+ const getIcon = () => (
+
+
+
+ );
+
return (
-
-
-
-
-
-
- {serviceNameValue}
-
+
);
};
diff --git a/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx b/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx
index 9e45892070120..7e13baf8ddcf9 100644
--- a/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx
+++ b/src/plugins/discover/public/context_awareness/profile_providers/common/logs_data_source_profile/accessors/get_cell_renderers.tsx
@@ -31,8 +31,8 @@ export const getCellRenderers: DataSourceProfileProvider['profile']['getCellRend
...SERVICE_NAME_FIELDS.reduce(
(acc, field) => ({
...acc,
- [field]: getServiceNameCell(field),
- [`${field}.keyword`]: getServiceNameCell(`${field}.keyword`),
+ [field]: getServiceNameCell(field, params),
+ [`${field}.keyword`]: getServiceNameCell(`${field}.keyword`, params),
}),
{}
),
diff --git a/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts b/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts
index cb66afc7ebc57..e18f6c5860dd2 100644
--- a/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts
+++ b/test/functional/apps/discover/context_awareness/extensions/_get_cell_renderers.ts
@@ -105,8 +105,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0);
const lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 0);
- const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java');
- const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown');
+ const firstServiceNameCell = await firstCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
+ const lastServiceNameCell = await lastCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
expect(await firstServiceNameCell.getVisibleText()).to.be('product');
expect(await lastServiceNameCell.getVisibleText()).to.be('accounting');
});
@@ -130,7 +134,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0);
expect(await firstCell.getVisibleText()).to.be('product');
- await testSubjects.missingOrFail('*serviceNameCell*');
+ await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name');
});
});
});
@@ -278,8 +282,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 1);
lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 1);
- const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java');
- const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown');
+ const firstServiceNameCell = await firstCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
+ const lastServiceNameCell = await lastCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
expect(await firstServiceNameCell.getVisibleText()).to.be('product');
expect(await lastServiceNameCell.getVisibleText()).to.be('accounting');
});
@@ -309,7 +317,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await firstCell.getVisibleText()).to.be('product');
expect(await lastCell.getVisibleText()).to.be('accounting');
- await testSubjects.missingOrFail('*serviceNameCell*');
+ await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name');
});
});
});
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 09d1f31eceb23..5028780367b9c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1188,6 +1188,8 @@
"@kbn/management-storybook-config/*": ["packages/kbn-management/storybook/config/*"],
"@kbn/management-test-plugin": ["test/plugin_functional/plugins/management_test_plugin"],
"@kbn/management-test-plugin/*": ["test/plugin_functional/plugins/management_test_plugin/*"],
+ "@kbn/manifest": ["packages/kbn-manifest"],
+ "@kbn/manifest/*": ["packages/kbn-manifest/*"],
"@kbn/mapbox-gl": ["packages/kbn-mapbox-gl"],
"@kbn/mapbox-gl/*": ["packages/kbn-mapbox-gl/*"],
"@kbn/maps-custom-raster-source-plugin": ["x-pack/examples/third_party_maps_source_example"],
diff --git a/x-pack/packages/security-solution/upselling/messages/index.tsx b/x-pack/packages/security-solution/upselling/messages/index.tsx
index 722a711995d01..4bda9477f13c0 100644
--- a/x-pack/packages/security-solution/upselling/messages/index.tsx
+++ b/x-pack/packages/security-solution/upselling/messages/index.tsx
@@ -46,3 +46,11 @@ export const ALERT_SUPPRESSION_RULE_DETAILS = i18n.translate(
'Alert suppression is configured but will not be applied due to insufficient licensing',
}
);
+
+export const UPGRADE_NOTES_MANAGEMENT_USER_FILTER = (requiredLicense: string) =>
+ i18n.translate('securitySolutionPackages.noteManagement.userFilter.upsell', {
+ defaultMessage: 'Upgrade to {requiredLicense} to make use of user filters',
+ values: {
+ requiredLicense,
+ },
+ });
diff --git a/x-pack/packages/security-solution/upselling/service/types.ts b/x-pack/packages/security-solution/upselling/service/types.ts
index 43019271a7e02..b053c9aedf857 100644
--- a/x-pack/packages/security-solution/upselling/service/types.ts
+++ b/x-pack/packages/security-solution/upselling/service/types.ts
@@ -27,4 +27,5 @@ export type UpsellingMessageId =
| 'investigation_guide_interactions'
| 'alert_assignments'
| 'alert_suppression_rule_form'
- | 'alert_suppression_rule_details';
+ | 'alert_suppression_rule_details'
+ | 'note_management_user_filter';
diff --git a/x-pack/plugins/fleet/common/constants/routes.ts b/x-pack/plugins/fleet/common/constants/routes.ts
index 9b5c35c3b3ce2..c071c6feecbf8 100644
--- a/x-pack/plugins/fleet/common/constants/routes.ts
+++ b/x-pack/plugins/fleet/common/constants/routes.ts
@@ -81,6 +81,8 @@ export const AGENT_POLICY_API_ROUTES = {
DELETE_PATTERN: `${AGENT_POLICY_API_ROOT}/delete`,
FULL_INFO_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/full`,
FULL_INFO_DOWNLOAD_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/download`,
+ LIST_OUTPUTS_PATTERN: `${AGENT_POLICY_API_ROOT}/outputs`,
+ INFO_OUTPUTS_PATTERN: `${AGENT_POLICY_API_ROOT}/{agentPolicyId}/outputs`,
};
// Kubernetes Manifest API routes
diff --git a/x-pack/plugins/fleet/common/services/routes.ts b/x-pack/plugins/fleet/common/services/routes.ts
index ff1fb4a5ff693..520a71e1bdc0a 100644
--- a/x-pack/plugins/fleet/common/services/routes.ts
+++ b/x-pack/plugins/fleet/common/services/routes.ts
@@ -197,6 +197,14 @@ export const agentPolicyRouteService = {
getResetAllPreconfiguredAgentPolicyPath: () => {
return PRECONFIGURATION_API_ROUTES.RESET_PATTERN;
},
+
+ getInfoOutputsPath: (agentPolicyId: string) => {
+ return AGENT_POLICY_API_ROUTES.INFO_OUTPUTS_PATTERN.replace('{agentPolicyId}', agentPolicyId);
+ },
+
+ getListOutputsPath: () => {
+ return AGENT_POLICY_API_ROUTES.LIST_OUTPUTS_PATTERN;
+ },
};
export const dataStreamRouteService = {
diff --git a/x-pack/plugins/fleet/common/types/models/agent_policy.ts b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
index ebb1aa3afe7f1..ba1a0b182af72 100644
--- a/x-pack/plugins/fleet/common/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/types/models/agent_policy.ts
@@ -262,3 +262,24 @@ export interface AgentlessApiResponse {
id: string;
region_id: string;
}
+
+// Definitions for agent policy outputs endpoints
+export interface MinimalOutput {
+ name?: string;
+ id?: string;
+}
+export interface IntegrationsOutput extends MinimalOutput {
+ pkgName?: string;
+ integrationPolicyName?: string;
+}
+
+export interface OutputsForAgentPolicy {
+ agentPolicyId?: string;
+ monitoring: {
+ output: MinimalOutput;
+ };
+ data: {
+ output: MinimalOutput;
+ integrations?: IntegrationsOutput[];
+ };
+}
diff --git a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts
index 42f44f7c7271e..7432d1d00e61e 100644
--- a/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts
+++ b/x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts
@@ -5,7 +5,12 @@
* 2.0.
*/
-import type { AgentPolicy, NewAgentPolicy, FullAgentPolicy } from '../models';
+import type {
+ AgentPolicy,
+ NewAgentPolicy,
+ FullAgentPolicy,
+ OutputsForAgentPolicy,
+} from '../models';
import type { ListResult, ListWithKuery, BulkGetResult } from './common';
@@ -93,3 +98,16 @@ export type FetchAllAgentPoliciesOptions = Pick<
export type FetchAllAgentPolicyIdsOptions = Pick & {
spaceId?: string;
};
+
+export interface GetAgentPolicyOutputsResponse {
+ item: OutputsForAgentPolicy;
+}
+export interface GetListAgentPolicyOutputsResponse {
+ items: OutputsForAgentPolicy[];
+}
+
+export interface GetListAgentPolicyOutputsRequest {
+ body: {
+ ids?: string[];
+ };
+}
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
index 1497b1bb0589e..6b0a7c512d197 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx
@@ -53,7 +53,7 @@ import { UninstallCommandFlyout } from '../../../../../../components';
import type { ValidationResults } from '../agent_policy_validation';
import { ExperimentalFeaturesService } from '../../../../services';
-
+import { useAgentPolicyFormContext } from '../agent_policy_form';
import { policyHasEndpointSecurity as hasElasticDefend } from '../../../../../../../common/services';
import {
@@ -127,6 +127,8 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent =
const isManagedorAgentlessPolicy =
agentPolicy.is_managed === true || agentPolicy?.supports_agentless === true;
+ const agentPolicyFormContect = useAgentPolicyFormContext();
+
const AgentTamperProtectionSectionContent = useMemo(
() => (
=
/>
}
>
-
- {
+ if (newValue.length === 0) {
+ return;
}
- onChange={(newValue) => {
- if (newValue.length === 0) {
- return;
- }
- updateAgentPolicy({
- space_ids: newValue,
- });
- }}
- />
-
+ updateAgentPolicy({
+ space_ids: newValue,
+ });
+ }}
+ />
) : null}
{
+ beforeEach(() => {
+ jest.mocked(useAgentPoliciesSpaces).mockReturnValue({
+ data: {
+ items: [
+ {
+ name: 'Default',
+ id: 'default',
+ },
+ {
+ name: 'Test',
+ id: 'test',
+ },
+ ],
+ },
+ } as any);
+ });
+ function render() {
+ const renderer = createFleetTestRendererMock();
+ const onChange = jest.fn();
+ const setInvalidSpaceError = jest.fn();
+ const result = renderer.render(
+
+ );
+
+ return {
+ result,
+ onChange,
+ setInvalidSpaceError,
+ };
+ }
+
+ it('should render invalid space errors', () => {
+ const { result, onChange, setInvalidSpaceError } = render();
+ const inputEl = result.getByTestId('comboBoxSearchInput');
+ fireEvent.change(inputEl, {
+ target: { value: 'invalidSpace' },
+ });
+ fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' });
+ expect(result.container).toHaveTextContent('invalidSpace is not a valid space.');
+ expect(onChange).not.toBeCalled();
+ expect(setInvalidSpaceError).toBeCalledWith(true);
+ });
+
+ it('should clear invalid space errors', () => {
+ const { result, setInvalidSpaceError } = render();
+ const inputEl = result.getByTestId('comboBoxSearchInput');
+ fireEvent.change(inputEl, {
+ target: { value: 'invalidSpace' },
+ });
+ fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' });
+ expect(result.container).toHaveTextContent('invalidSpace is not a valid space.');
+ fireEvent.change(inputEl, {
+ target: { value: '' },
+ });
+ fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' });
+ expect(result.container).not.toHaveTextContent('invalidSpace is not a valid space.');
+ expect(setInvalidSpaceError).toBeCalledWith(false);
+ });
+
+ it('should accept valid space', () => {
+ const { result, onChange, setInvalidSpaceError } = render();
+ const inputEl = result.getByTestId('comboBoxSearchInput');
+ fireEvent.change(inputEl, {
+ target: { value: 'test' },
+ });
+ fireEvent.keyDown(inputEl, { key: 'Enter', code: 'Enter' });
+ expect(result.container).not.toHaveTextContent('test is not a valid space.');
+ expect(onChange).toBeCalledWith(['test']);
+ expect(setInvalidSpaceError).not.toBeCalledWith(true);
+ });
+});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx
index 0532c5306d50f..53c7ed1d8226d 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/space_selector.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { type EuiComboBoxOptionOption, EuiHealth } from '@elastic/eui';
+import { type EuiComboBoxOptionOption, EuiHealth, EuiFormRow } from '@elastic/eui';
import { EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
@@ -16,11 +16,19 @@ export interface SpaceSelectorProps {
value: string[];
onChange: (newVal: string[]) => void;
isDisabled?: boolean;
+ setInvalidSpaceError?: (hasError: boolean) => void;
}
-export const SpaceSelector: React.FC = ({ value, onChange, isDisabled }) => {
+export const SpaceSelector: React.FC = ({
+ setInvalidSpaceError,
+ value,
+ onChange,
+ isDisabled,
+}) => {
const res = useAgentPoliciesSpaces();
+ const [error, setError] = React.useState();
+
const renderOption = React.useCallback(
(option: any, searchValue: string, contentClassName: string) => (
@@ -57,20 +65,41 @@ export const SpaceSelector: React.FC = ({ value, onChange, i
}, [options, value, res.isInitialLoading]);
return (
- {
- onChange(newOptions.map(({ key }) => key as string));
- }}
- />
+ key="space"
+ error={error}
+ isDisabled={isDisabled}
+ isInvalid={Boolean(error)}
+ >
+ {
+ const newError =
+ searchValue.length === 0 || hasMatchingOptions
+ ? undefined
+ : i18n.translate('xpack.fleet.agentPolicies.spaceSelectorInvalid', {
+ defaultMessage: '{space} is not a valid space.',
+ values: { space: searchValue },
+ });
+ setError(newError);
+ if (setInvalidSpaceError) {
+ setInvalidSpaceError(!!newError);
+ }
+ }}
+ onChange={(newOptions) => {
+ onChange(newOptions.map(({ key }) => key as string));
+ }}
+ />
+
);
};
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
index b437d61f64c58..8e97afcaa4d66 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_form.tsx
@@ -45,12 +45,14 @@ interface Props {
isEditing?: boolean;
// form error state is passed up to the form
updateAdvancedSettingsHasErrors: (hasErrors: boolean) => void;
+ setInvalidSpaceError: (hasErrors: boolean) => void;
}
const AgentPolicyFormContext = React.createContext<
| {
agentPolicy: Partial & { [key: string]: any };
updateAgentPolicy: (u: Partial) => void;
updateAdvancedSettingsHasErrors: (hasErrors: boolean) => void;
+ setInvalidSpaceError: (hasErrors: boolean) => void;
}
| undefined
>(undefined);
@@ -67,6 +69,7 @@ export const AgentPolicyForm: React.FunctionComponent = ({
validation,
isEditing = false,
updateAdvancedSettingsHasErrors,
+ setInvalidSpaceError,
}) => {
const authz = useAuthz();
const isDisabled = !authz.fleet.allAgentPolicies;
@@ -97,7 +100,12 @@ export const AgentPolicyForm: React.FunctionComponent = ({
return (
{!isEditing ? (
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
index 6e4f1e06b45a0..91cd710db4343 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/details_page/components/settings/index.tsx
@@ -90,6 +90,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
allowedNamespacePrefixes: spaceSettings?.allowedNamespacePrefixes,
});
const [hasAdvancedSettingsErrors, setHasAdvancedSettingsErrors] = useState(false);
+ const [hasInvalidSpaceError, setInvalidSpaceError] = useState(false);
const updateAgentPolicy = (updatedFields: Partial) => {
setAgentPolicy({
@@ -183,6 +184,7 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
validation={validation}
isEditing={true}
updateAdvancedSettingsHasErrors={setHasAdvancedSettingsErrors}
+ setInvalidSpaceError={setInvalidSpaceError}
/>
{hasChanges ? (
@@ -219,7 +221,8 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
isDisabled={
isLoading ||
Object.keys(validation).length > 0 ||
- hasAdvancedSettingsErrors
+ hasAdvancedSettingsErrors ||
+ hasInvalidSpaceError
}
btnProps={{
color: 'text',
@@ -242,7 +245,8 @@ export const SettingsView = memo<{ agentPolicy: AgentPolicy }>(
!hasAllAgentPoliciesPrivileges ||
isLoading ||
Object.keys(validation).length > 0 ||
- hasAdvancedSettingsErrors
+ hasAdvancedSettingsErrors ||
+ hasInvalidSpaceError
}
data-test-subj="agentPolicyDetailsSaveButton"
iconType="save"
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
index f147f7e112ea1..a5538e7e0fa30 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/list_page/components/create_agent_policy.tsx
@@ -61,6 +61,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({
allowedNamespacePrefixes: spaceSettings?.allowedNamespacePrefixes,
});
const [hasAdvancedSettingsErrors, setHasAdvancedSettingsErrors] = useState(false);
+ const [hasInvalidSpaceError, setInvalidSpaceError] = useState(false);
const updateAgentPolicy = (updatedFields: Partial) => {
setAgentPolicy({
@@ -104,6 +105,7 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({
updateSysMonitoring={(newValue) => setWithSysMonitoring(newValue)}
validation={validation}
updateAdvancedSettingsHasErrors={setHasAdvancedSettingsErrors}
+ setInvalidSpaceError={setInvalidSpaceError}
/>
);
@@ -130,7 +132,10 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({
0 || hasAdvancedSettingsErrors
+ isLoading ||
+ Object.keys(validation).length > 0 ||
+ hasAdvancedSettingsErrors ||
+ hasInvalidSpaceError
}
description={i18n.translate(
'xpack.fleet.createAgentPolicy.devtoolsRequestDescription',
@@ -150,7 +155,8 @@ export const CreateAgentPolicyFlyout: React.FunctionComponent = ({
!hasFleetAllAgentPoliciesPrivileges ||
isLoading ||
Object.keys(validation).length > 0 ||
- hasAdvancedSettingsErrors
+ hasAdvancedSettingsErrors ||
+ hasInvalidSpaceError
}
onClick={async () => {
setIsLoading(true);
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx
index 35fd048cc13cd..2c4113c003841 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_details/agent_details_overview.tsx
@@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n-react';
import type { Agent, AgentPolicy } from '../../../../../types';
-import { useAgentVersion } from '../../../../../hooks';
+import { useAgentVersion, useGetInfoOutputsForPolicy } from '../../../../../hooks';
import { ExperimentalFeaturesService, isAgentUpgradeable } from '../../../../../services';
import { AgentPolicySummaryLine } from '../../../../../components';
import { AgentHealth } from '../../../components';
@@ -30,6 +30,7 @@ import { Tags } from '../../../components/tags';
import { formatAgentCPU, formatAgentMemory } from '../../../services/agent_metrics';
import { AgentDashboardLink } from '../agent_dashboard_link';
import { AgentUpgradeStatus } from '../../../agent_list_page/components/agent_upgrade_status';
+import { AgentPolicyOutputsSummary } from '../../../agent_list_page/components/agent_policy_outputs_summary';
// Allows child text to be truncated
const FlexItemWithMinWidth = styled(EuiFlexItem)`
@@ -43,10 +44,17 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
const latestAgentVersion = useAgentVersion();
const { displayAgentMetrics } = ExperimentalFeaturesService.get();
+ const outputRes = useGetInfoOutputsForPolicy(agentPolicy?.id);
+ const outputs = outputRes?.data?.item;
+
return (
-
+
{displayAgentMetrics && (
@@ -206,6 +214,22 @@ export const AgentDetailsOverviewSection: React.FunctionComponent<{
? agent.local_metadata.host.id
: '-',
},
+ {
+ title: i18n.translate('xpack.fleet.agentDetails.outputForMonitoringLabel', {
+ defaultMessage: 'Output for integrations',
+ }),
+ description: outputs ? : '-',
+ },
+ {
+ title: i18n.translate('xpack.fleet.agentDetails.outputForMonitoringLabel', {
+ defaultMessage: 'Output for monitoring',
+ }),
+ description: outputs ? (
+
+ ) : (
+ '-'
+ ),
+ },
{
title: i18n.translate('xpack.fleet.agentDetails.logLevel', {
defaultMessage: 'Logging level',
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx
index 4c6c83dd7145e..d70ed67247207 100644
--- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_list_table.tsx
@@ -4,7 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
-import React from 'react';
+import React, { useCallback, useMemo } from 'react';
+import type { EuiBasicTableColumn } from '@elastic/eui';
import { type CriteriaWithPagination } from '@elastic/eui';
import {
EuiBasicTable,
@@ -23,20 +24,31 @@ import { isAgentUpgradeable, ExperimentalFeaturesService } from '../../../../ser
import { AgentHealth } from '../../components';
import type { Pagination } from '../../../../hooks';
-import { useAgentVersion } from '../../../../hooks';
+import { useAgentVersion, useGetListOutputsForPolicies } from '../../../../hooks';
import { useLink, useAuthz } from '../../../../hooks';
import { AgentPolicySummaryLine } from '../../../../components';
import { Tags } from '../../components/tags';
-import type { AgentMetrics } from '../../../../../../../common/types';
+import type { AgentMetrics, OutputsForAgentPolicy } from '../../../../../../../common/types';
import { formatAgentCPU, formatAgentMemory } from '../../services/agent_metrics';
+import { AgentPolicyOutputsSummary } from './agent_policy_outputs_summary';
+
import { AgentUpgradeStatus } from './agent_upgrade_status';
import { EmptyPrompt } from './empty_prompt';
-const VERSION_FIELD = 'local_metadata.elastic.agent.version';
-const HOSTNAME_FIELD = 'local_metadata.host.hostname';
+const AGENTS_TABLE_FIELDS = {
+ ACTIVE: 'active',
+ HOSTNAME: 'local_metadata.host.hostname',
+ POLICY: 'policy_id',
+ METRICS: 'metrics',
+ VERSION: 'local_metadata.elastic.agent.version',
+ LAST_CHECKIN: 'last_checkin',
+ OUTPUT_INTEGRATION: 'output_integrations',
+ OUTPUT_MONITORING: 'output_monitoring',
+};
+
function safeMetadata(val: any) {
if (typeof val !== 'string') {
return '-';
@@ -96,14 +108,33 @@ export const AgentListTable: React.FC = (props: Props) => {
const { getHref } = useLink();
const latestAgentVersion = useAgentVersion();
- const isAgentSelectable = (agent: Agent) => {
- if (!agent.active) return false;
- if (!agent.policy_id) return true;
+ const isAgentSelectable = useCallback(
+ (agent: Agent) => {
+ if (!agent.active) return false;
+ if (!agent.policy_id) return true;
- const agentPolicy = agentPoliciesIndexedById[agent.policy_id];
- const isHosted = agentPolicy?.is_managed === true;
- return !isHosted;
- };
+ const agentPolicy = agentPoliciesIndexedById[agent.policy_id];
+ const isHosted = agentPolicy?.is_managed === true;
+ return !isHosted;
+ },
+ [agentPoliciesIndexedById]
+ );
+
+ const agentsShown = useMemo(() => {
+ return totalAgents
+ ? showUpgradeable
+ ? agents.filter((agent) => isAgentSelectable(agent) && isAgentUpgradeable(agent))
+ : agents
+ : [];
+ }, [agents, isAgentSelectable, showUpgradeable, totalAgents]);
+
+ // get the policyIds of the agents shown on the page
+ const policyIds = useMemo(() => {
+ return agentsShown.map((agent) => agent?.policy_id ?? '');
+ }, [agentsShown]);
+ const allOutputs = useGetListOutputsForPolicies({
+ ids: policyIds,
+ });
const noItemsMessage =
isLoading && isCurrentRequestIncremented ? (
@@ -140,9 +171,9 @@ export const AgentListTable: React.FC = (props: Props) => {
},
};
- const columns = [
+ const columns: Array> = [
{
- field: 'active',
+ field: AGENTS_TABLE_FIELDS.ACTIVE,
sortable: false,
width: '85px',
name: i18n.translate('xpack.fleet.agentList.statusColumnTitle', {
@@ -151,7 +182,7 @@ export const AgentListTable: React.FC = (props: Props) => {
render: (active: boolean, agent: any) => ,
},
{
- field: HOSTNAME_FIELD,
+ field: AGENTS_TABLE_FIELDS.HOSTNAME,
sortable: true,
name: i18n.translate('xpack.fleet.agentList.hostColumnTitle', {
defaultMessage: 'Host',
@@ -171,7 +202,7 @@ export const AgentListTable: React.FC = (props: Props) => {
),
},
{
- field: 'policy_id',
+ field: AGENTS_TABLE_FIELDS.POLICY,
sortable: true,
truncateText: true,
name: i18n.translate('xpack.fleet.agentList.policyColumnTitle', {
@@ -208,7 +239,7 @@ export const AgentListTable: React.FC = (props: Props) => {
...(displayAgentMetrics
? [
{
- field: 'metrics',
+ field: AGENTS_TABLE_FIELDS.METRICS,
sortable: false,
name: (
= (props: Props) => {
),
},
{
- field: 'metrics',
+ field: AGENTS_TABLE_FIELDS.METRICS,
sortable: false,
name: (
= (props: Props) => {
},
]
: []),
-
{
- field: 'last_checkin',
+ field: AGENTS_TABLE_FIELDS.LAST_CHECKIN,
sortable: true,
name: i18n.translate('xpack.fleet.agentList.lastCheckinTitle', {
defaultMessage: 'Last activity',
}),
+ width: '100px',
+ render: (lastCheckin: string) =>
+ lastCheckin ? : undefined,
+ },
+ {
+ field: AGENTS_TABLE_FIELDS.OUTPUT_INTEGRATION,
+ sortable: true,
+ truncateText: true,
+ name: i18n.translate('xpack.fleet.agentList.integrationsOutputTitle', {
+ defaultMessage: 'Output for integrations',
+ }),
+ width: '180px',
+ render: (outputs: OutputsForAgentPolicy[], agent: Agent) => {
+ if (!agent?.policy_id) return null;
+
+ const outputsForPolicy = allOutputs?.data?.items.find(
+ (item) => item.agentPolicyId === agent?.policy_id
+ );
+ return ;
+ },
+ },
+ {
+ field: AGENTS_TABLE_FIELDS.OUTPUT_MONITORING,
+ sortable: true,
+ truncateText: true,
+ name: i18n.translate('xpack.fleet.agentList.monitoringOutputTitle', {
+ defaultMessage: 'Output for monitoring',
+ }),
width: '180px',
- render: (lastCheckin: string, agent: any) =>
- lastCheckin ? : null,
+ render: (outputs: OutputsForAgentPolicy[], agent: Agent) => {
+ if (!agent?.policy_id) return null;
+
+ const outputsForPolicy = allOutputs?.data?.items.find(
+ (item) => item.agentPolicyId === agent?.policy_id
+ );
+ return ;
+ },
},
{
- field: VERSION_FIELD,
+ field: AGENTS_TABLE_FIELDS.VERSION,
sortable: true,
width: '220px',
name: i18n.translate('xpack.fleet.agentList.versionTitle', {
@@ -322,13 +386,7 @@ export const AgentListTable: React.FC = (props: Props) => {
data-test-subj="fleetAgentListTable"
loading={isLoading}
noItemsMessage={noItemsMessage}
- items={
- totalAgents
- ? showUpgradeable
- ? agents.filter((agent) => isAgentSelectable(agent) && isAgentUpgradeable(agent))
- : agents
- : []
- }
+ items={agentsShown}
itemId="id"
columns={columns}
pagination={{
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.test.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.test.tsx
new file mode 100644
index 0000000000000..255b2efb94026
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.test.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 { act, fireEvent } from '@testing-library/react';
+import React from 'react';
+
+import { createFleetTestRendererMock } from '../../../../../../mock';
+import type { TestRenderer } from '../../../../../../mock';
+
+import type { OutputsForAgentPolicy } from '../../../../../../../common/types';
+
+import { AgentPolicyOutputsSummary } from './agent_policy_outputs_summary';
+
+describe('MultipleAgentPolicySummaryLine', () => {
+ let testRenderer: TestRenderer;
+ const outputsForPolicy: OutputsForAgentPolicy = {
+ agentPolicyId: 'policy-1',
+ monitoring: {
+ output: {
+ id: 'elasticsearch1',
+ name: 'Elasticsearch1',
+ },
+ },
+ data: {
+ output: {
+ id: 'elasticsearch1',
+ name: 'Elasticsearch1',
+ },
+ },
+ };
+ const data = {
+ data: {
+ output: {
+ id: 'elasticsearch1',
+ name: 'Elasticsearch1',
+ },
+ integrations: [
+ {
+ id: 'remote_es1',
+ name: 'Remote ES',
+ pkgName: 'ngnix',
+ integrationPolicyName: 'Nginx-1',
+ },
+
+ {
+ id: 'logstash',
+ name: 'Logstash-1',
+ pkgName: 'apache',
+ integrationPolicyName: 'Apache-1',
+ },
+ ],
+ },
+ };
+
+ const render = (outputs?: OutputsForAgentPolicy, isMonitoring?: boolean) =>
+ testRenderer.render(
+
+ );
+
+ beforeEach(() => {
+ testRenderer = createFleetTestRendererMock();
+ });
+
+ test('it should render the name associated with the default output when the agent policy does not have custom outputs', async () => {
+ const results = render(outputsForPolicy);
+ expect(results.container.textContent).toBe('Elasticsearch1');
+ expect(results.queryByTestId('outputNameLink')).toBeInTheDocument();
+ expect(results.queryByTestId('outputsIntegrationsNumberBadge')).not.toBeInTheDocument();
+ });
+
+ test('it should render the first output name and the badge when there are multiple outputs associated with integrations', async () => {
+ const results = render({ ...outputsForPolicy, ...data });
+
+ expect(results.queryByTestId('outputNameLink')).toBeInTheDocument();
+ expect(results.queryByTestId('outputsIntegrationsNumberBadge')).toBeInTheDocument();
+
+ await act(async () => {
+ fireEvent.click(results.getByTestId('outputsIntegrationsNumberBadge'));
+ });
+ expect(results.queryByTestId('outputPopover')).toBeInTheDocument();
+ expect(results.queryByTestId('output-integration-0')?.textContent).toContain(
+ 'Nginx-1: Remote ES'
+ );
+ expect(results.queryByTestId('output-integration-1')?.textContent).toContain(
+ 'Apache-1: Logstash-1'
+ );
+ });
+
+ test('it should not render the badge when monitoring is true', async () => {
+ const results = render({ ...outputsForPolicy, ...data }, true);
+
+ expect(results.queryByTestId('outputNameLink')).toBeInTheDocument();
+ expect(results.queryByTestId('outputsIntegrationsNumberBadge')).not.toBeInTheDocument();
+ });
+});
diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.tsx
new file mode 100644
index 0000000000000..c0b0e5fbfbccc
--- /dev/null
+++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_list_page/components/agent_policy_outputs_summary.tsx
@@ -0,0 +1,115 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import styled from 'styled-components';
+import React, { useMemo, useState } from 'react';
+
+import type { EuiListGroupItemProps } from '@elastic/eui';
+import {
+ EuiBadge,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLink,
+ EuiListGroup,
+ EuiPopover,
+ EuiPopoverTitle,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+import { useLink } from '../../../../hooks';
+import type { OutputsForAgentPolicy } from '../../../../../../../common/types';
+
+const TruncatedEuiLink = styled(EuiLink)`
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 120px;
+`;
+
+export const AgentPolicyOutputsSummary: React.FC<{
+ outputs?: OutputsForAgentPolicy;
+ isMonitoring?: boolean;
+}> = ({ outputs, isMonitoring }) => {
+ const { getHref } = useLink();
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
+ const closePopover = () => setIsPopoverOpen(false);
+
+ const monitoring = outputs?.monitoring;
+ const data = outputs?.data;
+
+ const listItems: EuiListGroupItemProps[] = useMemo(() => {
+ if (!data?.integrations) return [];
+
+ return (data?.integrations || []).map((integration, index) => {
+ return {
+ 'data-test-subj': `output-integration-${index}`,
+ label: `${integration.integrationPolicyName}: ${integration.name}`,
+ href: getHref('settings_edit_outputs', { outputId: integration?.id ?? '' }),
+ iconType: 'dot',
+ };
+ });
+ }, [getHref, data?.integrations]);
+
+ return (
+
+ {isMonitoring ? (
+
+
+ {monitoring?.output.name}
+
+
+ ) : (
+
+
+ {data?.output.name}
+
+
+ )}
+
+ {data?.integrations && data?.integrations.length >= 1 && !isMonitoring && (
+
+ setIsPopoverOpen(!isPopoverOpen)}
+ onClickAriaLabel="Open output integrations popover"
+ >
+ +{data?.integrations.length}
+
+
+
+ {i18n.translate('xpack.fleet.AgentPolicyOutputsSummary.popover.title', {
+ defaultMessage: 'Output for integrations',
+ })}
+
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts
index 9e4fb2344fc29..e130eae49c6eb 100644
--- a/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_request/agent_policy.ts
@@ -23,6 +23,9 @@ import type {
DeleteAgentPolicyRequest,
DeleteAgentPolicyResponse,
BulkGetAgentPoliciesResponse,
+ GetAgentPolicyOutputsResponse,
+ GetListAgentPolicyOutputsResponse,
+ GetListAgentPolicyOutputsRequest,
} from '../../types';
import { useRequest, sendRequest, useConditionalRequest, sendRequestForRq } from './use_request';
@@ -201,3 +204,21 @@ export const sendResetAllPreconfiguredAgentPolicies = () => {
version: API_VERSIONS.internal.v1,
});
};
+
+export const useGetListOutputsForPolicies = (body?: GetListAgentPolicyOutputsRequest['body']) => {
+ return useRequest({
+ path: agentPolicyRouteService.getListOutputsPath(),
+ method: 'post',
+ body: JSON.stringify(body),
+ version: API_VERSIONS.public.v1,
+ });
+};
+
+export const useGetInfoOutputsForPolicy = (agentPolicyId: string | undefined) => {
+ return useConditionalRequest({
+ path: agentPolicyId ? agentPolicyRouteService.getInfoOutputsPath(agentPolicyId) : undefined,
+ method: 'get',
+ shouldSendRequest: !!agentPolicyId,
+ version: API_VERSIONS.public.v1,
+ } as SendConditionalRequestConfig);
+};
diff --git a/x-pack/plugins/fleet/public/types/index.ts b/x-pack/plugins/fleet/public/types/index.ts
index 099df2ce5a34f..0f0adaba20b5d 100644
--- a/x-pack/plugins/fleet/public/types/index.ts
+++ b/x-pack/plugins/fleet/public/types/index.ts
@@ -147,6 +147,9 @@ export type {
GetEnrollmentSettingsRequest,
GetEnrollmentSettingsResponse,
GetSpaceSettingsResponse,
+ GetAgentPolicyOutputsResponse,
+ GetListAgentPolicyOutputsRequest,
+ GetListAgentPolicyOutputsResponse,
} from '../../common/types';
export {
entries,
diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
index 2c93880c10609..49b5590a2e761 100644
--- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
@@ -33,6 +33,8 @@ import type {
BulkGetAgentPoliciesRequestSchema,
AgentPolicy,
FleetRequestHandlerContext,
+ GetAgentPolicyOutputsRequestSchema,
+ GetListAgentPolicyOutputsRequestSchema,
} from '../../types';
import type {
@@ -47,6 +49,8 @@ import type {
GetFullAgentConfigMapResponse,
GetFullAgentManifestResponse,
BulkGetAgentPoliciesResponse,
+ GetAgentPolicyOutputsResponse,
+ GetListAgentPolicyOutputsResponse,
} from '../../../common/types';
import {
defaultFleetErrorHandler,
@@ -678,3 +682,64 @@ export const downloadK8sManifest: FleetRequestHandler<
return defaultFleetErrorHandler({ error, response });
}
};
+
+export const GetAgentPolicyOutputsHandler: FleetRequestHandler<
+ TypeOf,
+ undefined
+> = async (context, request, response) => {
+ try {
+ const coreContext = await context.core;
+ const soClient = coreContext.savedObjects.client;
+ const agentPolicy = await agentPolicyService.get(soClient, request.params.agentPolicyId);
+
+ if (!agentPolicy) {
+ return response.customError({
+ statusCode: 404,
+ body: { message: 'Agent policy not found' },
+ });
+ }
+ const outputs = await agentPolicyService.getAllOutputsForPolicy(soClient, agentPolicy);
+
+ const body: GetAgentPolicyOutputsResponse = {
+ item: outputs,
+ };
+ return response.ok({
+ body,
+ });
+ } catch (error) {
+ return defaultFleetErrorHandler({ error, response });
+ }
+};
+
+export const GetListAgentPolicyOutputsHandler: FleetRequestHandler<
+ undefined,
+ undefined,
+ TypeOf
+> = async (context, request, response) => {
+ try {
+ const coreContext = await context.core;
+ const soClient = coreContext.savedObjects.client;
+ const { ids } = request.body;
+
+ if (!ids) {
+ return response.ok({
+ body: { items: [] },
+ });
+ }
+ const agentPolicies = await agentPolicyService.getByIDs(soClient, ids, {
+ withPackagePolicies: true,
+ });
+
+ const outputsList = await agentPolicyService.listAllOutputsForPolicies(soClient, agentPolicies);
+
+ const body: GetListAgentPolicyOutputsResponse = {
+ items: outputsList,
+ };
+
+ return response.ok({
+ body,
+ });
+ } catch (error) {
+ return defaultFleetErrorHandler({ error, response });
+ }
+};
diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts
index 2ed7079deceec..9311f0ae2acca 100644
--- a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts
+++ b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts
@@ -28,6 +28,10 @@ import {
GetFullAgentPolicyResponseSchema,
DownloadFullAgentPolicyResponseSchema,
GetK8sManifestResponseScheme,
+ GetAgentPolicyOutputsRequestSchema,
+ GetAgentPolicyOutputsResponseSchema,
+ GetListAgentPolicyOutputsResponseSchema,
+ GetListAgentPolicyOutputsRequestSchema,
} from '../../types';
import { K8S_API_ROUTES } from '../../../common/constants';
@@ -47,6 +51,8 @@ import {
downloadK8sManifest,
getK8sManifest,
bulkGetAgentPoliciesHandler,
+ GetAgentPolicyOutputsHandler,
+ GetListAgentPolicyOutputsHandler,
} from './handlers';
export const registerRoutes = (router: FleetAuthzRouter) => {
@@ -390,4 +396,62 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
},
downloadK8sManifest
);
+
+ router.versioned
+ .post({
+ path: AGENT_POLICY_API_ROUTES.LIST_OUTPUTS_PATTERN,
+ fleetAuthz: (authz) => {
+ return authz.fleet.readAgentPolicies && authz.fleet.readSettings;
+ },
+ description: `Get list of outputs associated with agent policies`,
+ options: {
+ tags: ['oas-tag:Elastic Agent policies'],
+ },
+ })
+ .addVersion(
+ {
+ version: API_VERSIONS.public.v1,
+ validate: {
+ request: GetListAgentPolicyOutputsRequestSchema,
+ response: {
+ 200: {
+ body: () => GetListAgentPolicyOutputsResponseSchema,
+ },
+ 400: {
+ body: genericErrorResponse,
+ },
+ },
+ },
+ },
+ GetListAgentPolicyOutputsHandler
+ );
+
+ router.versioned
+ .get({
+ path: AGENT_POLICY_API_ROUTES.INFO_OUTPUTS_PATTERN,
+ fleetAuthz: (authz) => {
+ return authz.fleet.readAgentPolicies && authz.fleet.readSettings;
+ },
+ description: `Get list of outputs associated with agent policy by policy id`,
+ options: {
+ tags: ['oas-tag:Elastic Agent policies'],
+ },
+ })
+ .addVersion(
+ {
+ version: API_VERSIONS.public.v1,
+ validate: {
+ request: GetAgentPolicyOutputsRequestSchema,
+ response: {
+ 200: {
+ body: () => GetAgentPolicyOutputsResponseSchema,
+ },
+ 400: {
+ body: genericErrorResponse,
+ },
+ },
+ },
+ },
+ GetAgentPolicyOutputsHandler
+ );
};
diff --git a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts
index fa5522d50802b..609c560906de2 100644
--- a/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policies/full_agent_policy.test.ts
@@ -109,7 +109,7 @@ jest.mock('../output', () => {
getDefaultDataOutputId: async () => 'test-id',
getDefaultMonitoringOutputId: async () => 'test-id',
get: (soClient: any, id: string): Output => OUTPUTS[id] || OUTPUTS['test-id'],
- bulkGet: async (soClient: any, ids: string[]): Promise
-
+
{
+ const original = jest.requireActual('react-redux');
+
+ return {
+ ...original,
+ useDispatch: () => mockDispatch,
+ };
+});
+
+describe('UserFilterDropdown', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (useSuggestUsers as jest.Mock).mockReturnValue({
+ isLoading: false,
+ data: [{ user: { username: 'test' } }, { user: { username: 'elastic' } }],
+ });
+ (useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => true });
+ (useUpsellingMessage as jest.Mock).mockReturnValue('upsellingMessage');
+ });
+
+ it('should render the component enabled', () => {
+ const { getByTestId } = render();
+
+ const dropdown = getByTestId(USER_SELECT_TEST_ID);
+
+ expect(dropdown).toBeInTheDocument();
+ expect(dropdown).not.toHaveClass('euiComboBox-isDisabled');
+ });
+
+ it('should render the dropdown disabled', async () => {
+ (useLicense as jest.Mock).mockReturnValue({ isPlatinumPlus: () => false });
+
+ const { getByTestId } = render();
+
+ expect(getByTestId(USER_SELECT_TEST_ID)).toHaveClass('euiComboBox-isDisabled');
+ });
+
+ it('should call the correct action when select a user', async () => {
+ const { getByTestId } = render();
+
+ const userSelect = getByTestId('comboBoxSearchInput');
+ userSelect.focus();
+
+ const option = await screen.findByText('test');
+ fireEvent.click(option);
+
+ expect(mockDispatch).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx b/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx
new file mode 100644
index 0000000000000..78f4ef6dd2ac8
--- /dev/null
+++ b/x-pack/plugins/security_solution/public/notes/components/user_filter_dropdown.tsx
@@ -0,0 +1,79 @@
+/*
+ * 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, { useMemo, useCallback, useState } from 'react';
+import { EuiComboBox, EuiToolTip } from '@elastic/eui';
+import { useDispatch } from 'react-redux';
+import type { UserProfileWithAvatar } from '@kbn/user-profile-components';
+import { i18n } from '@kbn/i18n';
+import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types';
+import { useLicense } from '../../common/hooks/use_license';
+import { useUpsellingMessage } from '../../common/hooks/use_upselling';
+import { USER_SELECT_TEST_ID } from './test_ids';
+import { useSuggestUsers } from '../../common/components/user_profiles/use_suggest_users';
+import { userFilterUsers } from '..';
+
+export const USERS_DROPDOWN = i18n.translate('xpack.securitySolution.notes.usersDropdownLabel', {
+ defaultMessage: 'Users',
+});
+
+export const UserFilterDropdown = React.memo(() => {
+ const dispatch = useDispatch();
+ const isPlatinumPlus = useLicense().isPlatinumPlus();
+ const upsellingMessage = useUpsellingMessage('note_management_user_filter');
+
+ const { isLoading, data } = useSuggestUsers({
+ searchTerm: '',
+ enabled: isPlatinumPlus,
+ });
+ const users = useMemo(
+ () =>
+ (data || []).map((userProfile: UserProfileWithAvatar) => ({
+ label: userProfile.user.full_name || userProfile.user.username,
+ })),
+ [data]
+ );
+
+ const [selectedUser, setSelectedUser] = useState>>();
+ const onChange = useCallback(
+ (user: Array>) => {
+ setSelectedUser(user);
+ dispatch(userFilterUsers(user.length > 0 ? user[0].label : ''));
+ },
+ [dispatch]
+ );
+
+ const dropdown = useMemo(
+ () => (
+
+ ),
+ [isLoading, isPlatinumPlus, onChange, selectedUser, users]
+ );
+
+ return (
+ <>
+ {isPlatinumPlus ? (
+ <>{dropdown}>
+ ) : (
+
+ {dropdown}
+
+ )}
+ >
+ );
+});
+
+UserFilterDropdown.displayName = 'UserFilterDropdown';
diff --git a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx
index b7fbdab3b5982..69f3c5dd4cc28 100644
--- a/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx
+++ b/x-pack/plugins/security_solution_ess/public/upselling/register_upsellings.tsx
@@ -12,6 +12,7 @@ import {
ALERT_SUPPRESSION_RULE_FORM,
UPGRADE_ALERT_ASSIGNMENTS,
UPGRADE_INVESTIGATION_GUIDE,
+ UPGRADE_NOTES_MANAGEMENT_USER_FILTER,
} from '@kbn/security-solution-upselling/messages';
import type {
MessageUpsellings,
@@ -132,4 +133,9 @@ export const upsellingMessages: UpsellingMessages = [
minimumLicenseRequired: 'platinum',
message: ALERT_SUPPRESSION_RULE_DETAILS,
},
+ {
+ id: 'note_management_user_filter',
+ minimumLicenseRequired: 'platinum',
+ message: UPGRADE_NOTES_MANAGEMENT_USER_FILTER('Platinum'),
+ },
];
diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_outputs.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_outputs.ts
new file mode 100644
index 0000000000000..74c5af6b0d811
--- /dev/null
+++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy_outputs.ts
@@ -0,0 +1,282 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import expect from '@kbn/expect';
+import { CreateAgentPolicyResponse } from '@kbn/fleet-plugin/common';
+import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
+
+export default function (providerContext: FtrProviderContext) {
+ const { getService } = providerContext;
+ const supertest = getService('supertest');
+ const esArchiver = getService('esArchiver');
+ const kibanaServer = getService('kibanaServer');
+ const fleetAndAgents = getService('fleetAndAgents');
+
+ const createOutput = async ({
+ name,
+ id,
+ type,
+ hosts,
+ }: {
+ name: string;
+ id: string;
+ type: string;
+ hosts: string[];
+ }): Promise => {
+ const res = await supertest
+ .post(`/api/fleet/outputs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ id,
+ name,
+ type,
+ hosts,
+ })
+ .expect(200);
+ return res.body.item.id;
+ };
+
+ const createAgentPolicy = async (
+ name: string,
+ id: string,
+ dataOutputId?: string,
+ monitoringOutputId?: string
+ ): Promise => {
+ const res = await supertest
+ .post(`/api/fleet/agent_policies`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name,
+ id,
+ namespace: 'default',
+ ...(dataOutputId ? { data_output_id: dataOutputId } : {}),
+ ...(monitoringOutputId ? { monitoring_output_id: monitoringOutputId } : {}),
+ })
+ .expect(200);
+ return res.body.item;
+ };
+
+ const createAgentPolicyWithPackagePolicy = async ({
+ name,
+ id,
+ outputId,
+ }: {
+ name: string;
+ id: string;
+ outputId?: string;
+ }): Promise => {
+ const { body: res } = await supertest
+ .post(`/api/fleet/agent_policies`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name,
+ namespace: 'default',
+ id,
+ })
+ .expect(200);
+
+ const agentPolicyWithPPId = res.item.id;
+ // package policy needs to have a custom output_id
+ await supertest
+ .post(`/api/fleet/package_policies`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ name: 'filetest-1',
+ description: '',
+ namespace: 'default',
+ ...(outputId ? { output_id: outputId } : {}),
+ policy_id: agentPolicyWithPPId,
+ inputs: [],
+ package: {
+ name: 'filetest',
+ title: 'For File Tests',
+ version: '0.1.0',
+ },
+ })
+ .expect(200);
+ return res.item;
+ };
+
+ let output1Id = '';
+ describe('fleet_agent_policies_outputs', () => {
+ describe('POST /api/fleet/agent_policies/outputs', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
+ await kibanaServer.savedObjects.cleanStandardList();
+ await fleetAndAgents.setup();
+
+ output1Id = await createOutput({
+ name: 'Output 1',
+ id: 'logstash-output-1',
+ type: 'logstash',
+ hosts: ['test.fr:443'],
+ });
+ });
+ after(async () => {
+ await supertest
+ .delete(`/api/fleet/outputs/${output1Id}`)
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ });
+
+ it('should get a list of outputs by agent policies', async () => {
+ await createAgentPolicy('Agent policy with default output', 'agent-policy-1');
+ await createAgentPolicy(
+ 'Agent policy with custom output',
+ 'agent-policy-2',
+ output1Id,
+ output1Id
+ );
+
+ const outputsPerPoliciesRes = await supertest
+ .post(`/api/fleet/agent_policies/outputs`)
+ .set('kbn-xsrf', 'xxxx')
+ .send({
+ ids: ['agent-policy-1', 'agent-policy-2'],
+ })
+ .expect(200);
+ expect(outputsPerPoliciesRes.body.items).to.eql([
+ {
+ agentPolicyId: 'agent-policy-1',
+ monitoring: {
+ output: {
+ name: 'default',
+ id: 'fleet-default-output',
+ },
+ },
+ data: {
+ output: {
+ name: 'default',
+ id: 'fleet-default-output',
+ },
+ integrations: [],
+ },
+ },
+ {
+ agentPolicyId: 'agent-policy-2',
+ monitoring: {
+ output: {
+ name: 'Output 1',
+ id: 'logstash-output-1',
+ },
+ },
+ data: {
+ output: {
+ name: 'Output 1',
+ id: 'logstash-output-1',
+ },
+ integrations: [],
+ },
+ },
+ ]);
+ // clean up policies
+ await supertest
+ .post(`/api/fleet/agent_policies/delete`)
+ .send({ agentPolicyId: 'agent-policy-1' })
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ await supertest
+ .post(`/api/fleet/agent_policies/delete`)
+ .send({ agentPolicyId: 'agent-policy-2' })
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ });
+ });
+
+ let output2Id = '';
+ describe('GET /api/fleet/agent_policies/{agentPolicyId}/outputs', () => {
+ before(async () => {
+ await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server');
+ await kibanaServer.savedObjects.cleanStandardList();
+ await fleetAndAgents.setup();
+
+ output2Id = await createOutput({
+ name: 'ES Output 1',
+ id: 'es-output-1',
+ type: 'elasticsearch',
+ hosts: ['https://test.fr:8080'],
+ });
+ });
+ after(async () => {
+ await supertest
+ .delete(`/api/fleet/outputs/${output2Id}`)
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ });
+
+ it('should get the list of outputs related to an agentPolicy id', async () => {
+ await createAgentPolicy('Agent policy with ES output', 'agent-policy-custom', output2Id);
+
+ const outputsPerPoliciesRes = await supertest
+ .get(`/api/fleet/agent_policies/agent-policy-custom/outputs`)
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ expect(outputsPerPoliciesRes.body.item).to.eql({
+ monitoring: {
+ output: {
+ name: 'default',
+ id: 'fleet-default-output',
+ },
+ },
+ data: {
+ output: {
+ name: 'ES Output 1',
+ id: 'es-output-1',
+ },
+ integrations: [],
+ },
+ });
+
+ await supertest
+ .post(`/api/fleet/agent_policies/delete`)
+ .send({ agentPolicyId: 'agent-policy-custom' })
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ });
+
+ it('should also list the outputs set on integrations if any', async () => {
+ await createAgentPolicyWithPackagePolicy({
+ name: 'Agent Policy with package policy',
+ id: 'agent-policy-custom-2',
+ outputId: output2Id,
+ });
+
+ const outputsPerPoliciesRes = await supertest
+ .get(`/api/fleet/agent_policies/agent-policy-custom-2/outputs`)
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ expect(outputsPerPoliciesRes.body.item).to.eql({
+ monitoring: {
+ output: {
+ name: 'default',
+ id: 'fleet-default-output',
+ },
+ },
+ data: {
+ output: {
+ name: 'default',
+ id: 'fleet-default-output',
+ },
+ integrations: [
+ {
+ id: 'es-output-1',
+ integrationPolicyName: 'filetest-1',
+ name: 'ES Output 1',
+ },
+ ],
+ },
+ });
+
+ await supertest
+ .post(`/api/fleet/agent_policies/delete`)
+ .send({ agentPolicyId: 'agent-policy-custom-2' })
+ .set('kbn-xsrf', 'xxxx')
+ .expect(200);
+ });
+ });
+ });
+}
diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/index.js b/x-pack/test/fleet_api_integration/apis/agent_policy/index.js
index 9ae58b0089942..b036ab9d8103d 100644
--- a/x-pack/test/fleet_api_integration/apis/agent_policy/index.js
+++ b/x-pack/test/fleet_api_integration/apis/agent_policy/index.js
@@ -13,5 +13,6 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./privileges'));
loadTestFile(require.resolve('./agent_policy_root_integrations'));
loadTestFile(require.resolve('./create_standalone_api_key'));
+ loadTestFile(require.resolve('./agent_policy_outputs'));
});
}
diff --git a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts
index 0cf8aeedd257a..b8503e0f8dcab 100644
--- a/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts
+++ b/x-pack/test_serverless/functional/test_suites/common/discover/context_awareness/extensions/_get_cell_renderers.ts
@@ -105,8 +105,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0);
const lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 0);
- const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java');
- const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown');
+ const firstServiceNameCell = await firstCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
+ const lastServiceNameCell = await lastCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
expect(await firstServiceNameCell.getVisibleText()).to.be('product');
expect(await lastServiceNameCell.getVisibleText()).to.be('accounting');
});
@@ -130,7 +134,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
const firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 0);
expect(await firstCell.getVisibleText()).to.be('product');
- await testSubjects.missingOrFail('*serviceNameCell*');
+ await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name');
});
});
});
@@ -277,8 +281,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await retry.try(async () => {
firstCell = await dataGrid.getCellElementExcludingControlColumns(0, 1);
lastCell = await dataGrid.getCellElementExcludingControlColumns(2, 1);
- const firstServiceNameCell = await firstCell.findByTestSubject('serviceNameCell-java');
- const lastServiceNameCell = await lastCell.findByTestSubject('serviceNameCell-unknown');
+ const firstServiceNameCell = await firstCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
+ const lastServiceNameCell = await lastCell.findByTestSubject(
+ 'dataTableCellActionsPopover_service.name'
+ );
expect(await firstServiceNameCell.getVisibleText()).to.be('product');
expect(await lastServiceNameCell.getVisibleText()).to.be('accounting');
});
@@ -308,7 +316,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
expect(await firstCell.getVisibleText()).to.be('product');
expect(await lastCell.getVisibleText()).to.be('accounting');
- await testSubjects.missingOrFail('*serviceNameCell*');
+ await testSubjects.missingOrFail('dataTableCellActionsPopover_service.name');
});
});
});
diff --git a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts
index f571fe4e0e462..ba12ebc153ca8 100644
--- a/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts
+++ b/x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer/data_source_selector.ts
@@ -214,25 +214,25 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('should sort the integrations list by the clicked sorting option', async () => {
// Test ascending order
- await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc');
-
await retry.try(async () => {
+ await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc');
+ await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc');
const { integrations } = await PageObjects.observabilityLogsExplorer.getIntegrations();
expect(integrations).to.eql(initialPackagesTexts);
});
// Test descending order
- await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc');
-
await retry.try(async () => {
+ await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc');
+ await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc');
const { integrations } = await PageObjects.observabilityLogsExplorer.getIntegrations();
expect(integrations).to.eql(initialPackagesTexts.slice().reverse());
});
// Test back ascending order
- await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc');
-
await retry.try(async () => {
+ await PageObjects.observabilityLogsExplorer.clickSortButtonBy('desc');
+ await PageObjects.observabilityLogsExplorer.clickSortButtonBy('asc');
const { integrations } = await PageObjects.observabilityLogsExplorer.getIntegrations();
expect(integrations).to.eql(initialPackagesTexts);
});
diff --git a/yarn.lock b/yarn.lock
index 0e0d6afb677c2..b5b1294c39f7e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5671,6 +5671,10 @@
version "0.0.0"
uid ""
+"@kbn/manifest@link:packages/kbn-manifest":
+ version "0.0.0"
+ uid ""
+
"@kbn/mapbox-gl@link:packages/kbn-mapbox-gl":
version "0.0.0"
uid ""