diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture
index 7fefbbb26fd12..e017ba54f5595 100644
--- a/.ci/Jenkinsfile_baseline_capture
+++ b/.ci/Jenkinsfile_baseline_capture
@@ -23,6 +23,7 @@ kibanaPipeline(timeoutMinutes: 210) {
) {
withGcpServiceAccount.fromVaultSecret('secret/kibana-issues/dev/ci-artifacts-key', 'value') {
withEnv([
+ 'BUILD_TS_REFS_DISABLE=false', // disabled in root config so we need to override that here
'BUILD_TS_REFS_CACHE_ENABLE=true',
'BUILD_TS_REFS_CACHE_CAPTURE=true',
'DISABLE_BOOTSTRAP_VALIDATION=true',
diff --git a/docs/apm/getting-started.asciidoc b/docs/apm/getting-started.asciidoc
index c185fdb43faf1..e448c0beb8b99 100644
--- a/docs/apm/getting-started.asciidoc
+++ b/docs/apm/getting-started.asciidoc
@@ -6,6 +6,24 @@
Get started
++++
+// Conditionally display a screenshot or video depending on what the
+// current documentation version is.
+
+ifeval::["{is-current-version}"=="true"]
+++++
+
+
+
+++++
+endif::[]
+
For a quick, high-level overview of the health and performance of your application,
start with:
diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc
index a3ac62a4c8343..99a6205ae010e 100644
--- a/docs/apm/service-maps.asciidoc
+++ b/docs/apm/service-maps.asciidoc
@@ -8,14 +8,34 @@ requests per minute, and errors per minute.
If enabled, service maps also integrate with machine learning--for real time health indicators based on anomaly detection scores.
All of these features can help you quickly and visually assess your services' status and health.
+// Conditionally display a screenshot or video depending on what the
+// current documentation version is.
+
+ifeval::["{is-current-version}"=="true"]
+++++
+
+
+
+++++
+endif::[]
+
+ifeval::["{is-current-version}"=="false"]
+[role="screenshot"]
+image::apm/images/service-maps.png[Example view of service maps in the APM app in Kibana]
+endif::[]
+
We currently surface two types of service maps:
* Global: All services instrumented with APM agents and the connections between them are shown.
* Service-specific: Highlight connections for a selected service.
-[role="screenshot"]
-image::apm/images/service-maps.png[Example view of service maps in the APM app in Kibana]
-
[float]
[[service-maps-how]]
=== How do service maps work?
diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc
index f48dbeab9d61a..6483442248cea 100644
--- a/docs/settings/monitoring-settings.asciidoc
+++ b/docs/settings/monitoring-settings.asciidoc
@@ -37,11 +37,6 @@ For more information, see
monitoring back-end does not run and {kib} stats are not sent to the monitoring
cluster.
-a|`monitoring.cluster_alerts.`
-`email_notifications.email_address` {ess-icon}
- | Specifies the email address where you want to receive cluster alerts.
- See <> for details.
-
| `monitoring.ui.elasticsearch.hosts`
| Specifies the location of the {es} cluster where your monitoring data is stored.
By default, this is the same as <>. This setting enables
diff --git a/docs/user/monitoring/cluster-alerts.asciidoc b/docs/user/monitoring/cluster-alerts.asciidoc
deleted file mode 100644
index 2945ebc67710c..0000000000000
--- a/docs/user/monitoring/cluster-alerts.asciidoc
+++ /dev/null
@@ -1,64 +0,0 @@
-[role="xpack"]
-[[cluster-alerts]]
-= Cluster Alerts
-
-The *Stack Monitoring > Clusters* page in {kib} summarizes the status of your
-{stack}. You can drill down into the metrics to view more information about your
-cluster and specific nodes, instances, and indices.
-
-The Top Cluster Alerts shown on the Clusters page notify you of
-conditions that require your attention:
-
-* {es} Cluster Health Status is Yellow (missing at least one replica)
-or Red (missing at least one primary).
-* {es} Version Mismatch. You have {es} nodes with
-different versions in the same cluster.
-* {kib} Version Mismatch. You have {kib} instances with different
-versions running against the same {es} cluster.
-* Logstash Version Mismatch. You have Logstash nodes with different
-versions reporting stats to the same monitoring cluster.
-* {es} Nodes Changed. You have {es} nodes that were recently added or removed.
-* {es} License Expiration. The cluster's license is about to expire.
-+
---
-If you do not preserve the data directory when upgrading a {kib} or
-Logstash node, the instance is assigned a new persistent UUID and shows up
-as a new instance
---
-* {xpack} License Expiration. When the {xpack} license expiration date
-approaches, you will get notifications with a severity level relative to how
-soon the expiration date is:
- ** 60 days: Informational alert
- ** 30 days: Low-level alert
- ** 15 days: Medium-level alert
- ** 7 days: Severe-level alert
-+
-The 60-day and 30-day thresholds are skipped for Trial licenses, which are only
-valid for 30 days.
-
-The {monitor-features} check the cluster alert conditions every minute. Cluster
-alerts are automatically dismissed when the condition is resolved.
-
-NOTE: {watcher} must be enabled to view cluster alerts. If you have a Basic
-license, Top Cluster Alerts are not displayed.
-
-[float]
-[[cluster-alert-email-notifications]]
-== Email Notifications
-To receive email notifications for the Cluster Alerts:
-
-. Configure an email account as described in
-{ref}/actions-email.html#configuring-email[Configuring email accounts].
-. Configure the
-`monitoring.cluster_alerts.email_notifications.email_address` setting in
-`kibana.yml` with your email address.
-+
---
-TIP: If you have separate production and monitoring clusters and separate {kib}
-instances for those clusters, you must put the
-`monitoring.cluster_alerts.email_notifications.email_address` setting in
-the {kib} instance that is associated with the production cluster.
-
---
-
-Email notifications are sent only when Cluster Alerts are triggered and resolved.
diff --git a/docs/user/monitoring/index.asciidoc b/docs/user/monitoring/index.asciidoc
index 514988792d214..e4fd4a8cd085c 100644
--- a/docs/user/monitoring/index.asciidoc
+++ b/docs/user/monitoring/index.asciidoc
@@ -1,6 +1,5 @@
include::xpack-monitoring.asciidoc[]
include::beats-details.asciidoc[leveloffset=+1]
-include::cluster-alerts.asciidoc[leveloffset=+1]
include::elasticsearch-details.asciidoc[leveloffset=+1]
include::kibana-alerts.asciidoc[leveloffset=+1]
include::kibana-details.asciidoc[leveloffset=+1]
diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc
index 300497126c3e5..04f4e986ca289 100644
--- a/docs/user/monitoring/kibana-alerts.asciidoc
+++ b/docs/user/monitoring/kibana-alerts.asciidoc
@@ -29,7 +29,7 @@ To review and modify all the available alerts, use
This alert is triggered when a node runs a consistently high CPU load. By
default, the trigger condition is set at 85% or more averaged over the last 5
minutes. The alert is grouped across all the nodes of the cluster by running
-checks on a schedule time of 1 minute with a re-notify internal of 1 day.
+checks on a schedule time of 1 minute with a re-notify interval of 1 day.
[discrete]
[[kibana-alerts-disk-usage-threshold]]
@@ -38,7 +38,7 @@ checks on a schedule time of 1 minute with a re-notify internal of 1 day.
This alert is triggered when a node is nearly at disk capacity. By
default, the trigger condition is set at 80% or more averaged over the last 5
minutes. The alert is grouped across all the nodes of the cluster by running
-checks on a schedule time of 1 minute with a re-notify internal of 1 day.
+checks on a schedule time of 1 minute with a re-notify interval of 1 day.
[discrete]
[[kibana-alerts-jvm-memory-threshold]]
@@ -47,7 +47,7 @@ checks on a schedule time of 1 minute with a re-notify internal of 1 day.
This alert is triggered when a node runs a consistently high JVM memory usage. By
default, the trigger condition is set at 85% or more averaged over the last 5
minutes. The alert is grouped across all the nodes of the cluster by running
-checks on a schedule time of 1 minute with a re-notify internal of 1 day.
+checks on a schedule time of 1 minute with a re-notify interval of 1 day.
[discrete]
[[kibana-alerts-missing-monitoring-data]]
@@ -56,7 +56,72 @@ checks on a schedule time of 1 minute with a re-notify internal of 1 day.
This alert is triggered when any stack products nodes or instances stop sending
monitoring data. By default, the trigger condition is set to missing for 15 minutes
looking back 1 day. The alert is grouped across all the nodes of the cluster by running
-checks on a schedule time of 1 minute with a re-notify internal of 6 hours.
+checks on a schedule time of 1 minute with a re-notify interval of 6 hours.
+
+[discrete]
+[[kibana-alerts-thread-pool-rejections]]
+== Thread pool rejections (search/write)
+
+This alert is triggered when a node experiences thread pool rejections. By
+default, the trigger condition is set at 300 or more over the last 5
+minutes. The alert is grouped across all the nodes of the cluster by running
+checks on a schedule time of 1 minute with a re-notify interval of 1 day.
+Thresholds can be set independently for `search` and `write` type rejections.
+
+[discrete]
+[[kibana-alerts-ccr-read-exceptions]]
+== CCR read exceptions
+
+This alert is triggered if a read exception has been detected on any of the
+replicated clusters. The trigger condition is met if 1 or more read exceptions
+are detected in the last hour. The alert is grouped across all replicated clusters
+by running checks on a schedule time of 1 minute with a re-notify interval of 6 hours.
+
+[discrete]
+[[kibana-alerts-large-shard-size]]
+== Large shard size
+
+This alert is triggered if a large (primary) shard size is found on any of the
+specified index patterns. The trigger condition is met if an index's shard size is
+55gb or higher in the last 5 minutes. The alert is grouped across all indices that match
+the default patter of `*` by running checks on a schedule time of 1 minute with a re-notify
+interval of 12 hours.
+
+[discrete]
+[[kibana-alerts-cluster-alerts]]
+== Cluster alerts
+
+These alerts summarize the current status of your {stack}. You can drill down into the metrics
+to view more information about your cluster and specific nodes, instances, and indices.
+
+An alert will be triggered if any of the following conditions are met within the last minute:
+
+* {es} cluster health status is yellow (missing at least one replica)
+or red (missing at least one primary).
+* {es} version mismatch. You have {es} nodes with
+different versions in the same cluster.
+* {kib} version mismatch. You have {kib} instances with different
+versions running against the same {es} cluster.
+* Logstash version mismatch. You have Logstash nodes with different
+versions reporting stats to the same monitoring cluster.
+* {es} nodes changed. You have {es} nodes that were recently added or removed.
+* {es} license expiration. The cluster's license is about to expire.
++
+--
+If you do not preserve the data directory when upgrading a {kib} or
+Logstash node, the instance is assigned a new persistent UUID and shows up
+as a new instance
+--
+* Subscription license expiration. When the expiration date
+approaches, you will get notifications with a severity level relative to how
+soon the expiration date is:
+ ** 60 days: Informational alert
+ ** 30 days: Low-level alert
+ ** 15 days: Medium-level alert
+ ** 7 days: Severe-level alert
++
+The 60-day and 30-day thresholds are skipped for Trial licenses, which are only
+valid for 30 days.
NOTE: Some action types are subscription features, while others are free.
For a comparison of the Elastic subscription levels, see the alerting section of
diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts
index 2835fb8370b8f..ef3172b620b23 100644
--- a/src/core/public/doc_links/doc_links_service.ts
+++ b/src/core/public/doc_links/doc_links_service.ts
@@ -218,12 +218,15 @@ export class DocLinksService {
guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/maps.html`,
},
monitoring: {
- alertsCluster: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/cluster-alerts.html`,
alertsKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html`,
alertsKibanaCpuThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cpu-threshold`,
alertsKibanaDiskThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-disk-usage-threshold`,
alertsKibanaJvmThreshold: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-jvm-memory-threshold`,
alertsKibanaMissingData: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-missing-monitoring-data`,
+ alertsKibanaThreadpoolRejections: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-thread-pool-rejections`,
+ alertsKibanaCCRReadExceptions: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-ccr-read-exceptions`,
+ alertsKibanaLargeShardSize: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-large-shard-size`,
+ alertsKibanaClusterAlerts: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/kibana-alerts.html#kibana-alerts-cluster-alerts`,
metricbeatBlog: `${ELASTIC_WEBSITE_URL}blog/external-collection-for-elastic-stack-monitoring-is-now-available-via-metricbeat`,
monitorElasticsearch: `${ELASTICSEARCH_DOCS}configuring-metricbeat.html`,
monitorKibana: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/monitoring-metricbeat.html`,
diff --git a/test/functional/apps/visualize/_heatmap_chart.ts b/test/functional/apps/visualize/_heatmap_chart.ts
index 660f45179631e..79a9a6cbd5aca 100644
--- a/test/functional/apps/visualize/_heatmap_chart.ts
+++ b/test/functional/apps/visualize/_heatmap_chart.ts
@@ -15,7 +15,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
const inspector = getService('inspector');
const PageObjects = getPageObjects(['visualize', 'visEditor', 'visChart', 'timePicker']);
- describe('heatmap chart', function indexPatternCreation() {
+ // FLAKY: https://github.com/elastic/kibana/issues/95642
+ describe.skip('heatmap chart', function indexPatternCreation() {
const vizName1 = 'Visualization HeatmapChart';
before(async function () {
diff --git a/test/scripts/checks/type_check.sh b/test/scripts/checks/type_check.sh
index 5e091625de4ed..a7ea84d4cadc7 100755
--- a/test/scripts/checks/type_check.sh
+++ b/test/scripts/checks/type_check.sh
@@ -2,5 +2,8 @@
source src/dev/ci_setup/setup_env.sh
+checks-reporter-with-killswitch "Build TS Refs" \
+ node scripts/build_ts_refs --ignore-type-failures --force
+
checks-reporter-with-killswitch "Check Types" \
node scripts/type_check
diff --git a/vars/workers.groovy b/vars/workers.groovy
index 5d3328bc8a3c4..1260f74f1bdf9 100644
--- a/vars/workers.groovy
+++ b/vars/workers.groovy
@@ -101,6 +101,7 @@ def base(Map params, Closure closure) {
"TEST_BROWSER_HEADLESS=1",
"GIT_BRANCH=${checkoutInfo.branch}",
"TMPDIR=${env.WORKSPACE}/tmp", // For Chrome and anything else that respects it
+ "BUILD_TS_REFS_DISABLE=true", // no need to build ts refs in bootstrap
]) {
withCredentials([
string(credentialsId: 'vault-addr', variable: 'VAULT_ADDR'),
diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx
index 1018b9eca2119..aeb2a2c6390fc 100644
--- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx
+++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx
@@ -70,7 +70,7 @@ function DebugQueryCallout() {
{i18n.translate(
'xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings',
- { defaultMessage: 'Advanced Setting' }
+ { defaultMessage: 'Advanced Settings' }
)}
),
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx
index 44bd8b78320d6..1ef522c0c0de0 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx
@@ -27,7 +27,7 @@ describe('IndexingStatus', () => {
const props = {
percentageComplete: 50,
numDocumentsWithErrors: 1,
- activeReindexJobId: 12,
+ activeReindexJobId: '12abc',
viewLinkPath: '/path',
statusPath: '/other_path',
itemId: '1',
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx
index ee0557e15396c..bd3eacacb04e5 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx
@@ -17,7 +17,7 @@ import { IndexingStatusContent } from './indexing_status_content';
import { IndexingStatusErrors } from './indexing_status_errors';
import { IndexingStatusLogic } from './indexing_status_logic';
-export interface IIndexingStatusProps {
+export interface IIndexingStatusProps extends IIndexingStatus {
viewLinkPath: string;
itemId: string;
statusPath: string;
@@ -26,12 +26,9 @@ export interface IIndexingStatusProps {
setGlobalIndexingStatus?(activeReindexJob: IIndexingStatus): void;
}
-export const IndexingStatus: React.FC = ({
- viewLinkPath,
- statusPath,
- onComplete,
-}) => {
- const { percentageComplete, numDocumentsWithErrors } = useValues(IndexingStatusLogic);
+export const IndexingStatus: React.FC = (props) => {
+ const { viewLinkPath, statusPath, onComplete } = props;
+ const { percentageComplete, numDocumentsWithErrors } = useValues(IndexingStatusLogic(props));
const { fetchIndexingStatus } = useActions(IndexingStatusLogic);
useEffect(() => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts
index 679d04b414984..468aeacf9e11a 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.test.ts
@@ -12,9 +12,9 @@ import { nextTick } from '@kbn/test/jest';
import { IndexingStatusLogic } from './indexing_status_logic';
describe('IndexingStatusLogic', () => {
- const { mount, unmount } = new LogicMounter(IndexingStatusLogic);
const { http } = mockHttpValues;
const { flashAPIErrors } = mockFlashMessageHelpers;
+ const { mount, unmount } = new LogicMounter(IndexingStatusLogic);
const mockStatusResponse = {
percentageComplete: 50,
@@ -24,7 +24,7 @@ describe('IndexingStatusLogic', () => {
beforeEach(() => {
jest.clearAllMocks();
- mount();
+ mount({}, { percentageComplete: 100, numDocumentsWithErrors: 0 });
});
it('has expected default values', () => {
@@ -51,6 +51,7 @@ describe('IndexingStatusLogic', () => {
jest.useFakeTimers();
const statusPath = '/api/workplace_search/path/123';
const onComplete = jest.fn();
+ const clearIntervalSpy = jest.spyOn(global, 'clearInterval');
const TIMEOUT = 3000;
it('calls API and sets values', async () => {
@@ -84,13 +85,13 @@ describe('IndexingStatusLogic', () => {
await nextTick();
- expect(clearInterval).toHaveBeenCalled();
+ expect(clearIntervalSpy).toHaveBeenCalled();
expect(onComplete).toHaveBeenCalledWith(mockStatusResponse.numDocumentsWithErrors);
});
it('handles unmounting', async () => {
unmount();
- expect(clearInterval).toHaveBeenCalled();
+ expect(clearIntervalSpy).toHaveBeenCalled();
});
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts
index a436b669bcbe5..a5f3f7ad3d067 100644
--- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts
@@ -32,6 +32,7 @@ interface IndexingStatusValues {
let pollingInterval: number;
export const IndexingStatusLogic = kea>({
+ path: ['enterprise_search', 'indexing_status_logic'],
actions: {
fetchIndexingStatus: ({ statusPath, onComplete }) => ({ statusPath, onComplete }),
setIndexingStatus: ({ numDocumentsWithErrors, percentageComplete }) => ({
@@ -39,20 +40,20 @@ export const IndexingStatusLogic = kea ({
percentageComplete: [
- 100,
+ props.percentageComplete,
{
setIndexingStatus: (_, { percentageComplete }) => percentageComplete,
},
],
numDocumentsWithErrors: [
- 0,
+ props.numDocumentsWithErrors,
{
setIndexingStatus: (_, { numDocumentsWithErrors }) => numDocumentsWithErrors,
},
],
- },
+ }),
listeners: ({ actions }) => ({
fetchIndexingStatus: ({ statusPath, onComplete }: IndexingStatusProps) => {
const { http } = HttpLogic.values;
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts
index 6c60cd74a9c9f..8ced90e7d7729 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts
@@ -458,7 +458,12 @@ describe('AddSourceLogic', () => {
AddSourceLogic.actions.getSourceReConnectData('github');
expect(http.get).toHaveBeenCalledWith(
- '/api/workplace_search/org/sources/github/reauth_prepare'
+ '/api/workplace_search/org/sources/github/reauth_prepare',
+ {
+ query: {
+ kibana_host: '',
+ },
+ }
);
await nextTick();
expect(setSourceConnectDataSpy).toHaveBeenCalledWith(sourceConnectData);
@@ -648,7 +653,12 @@ describe('AddSourceLogic', () => {
AddSourceLogic.actions.getSourceReConnectData('123');
expect(http.get).toHaveBeenCalledWith(
- '/api/workplace_search/account/sources/123/reauth_prepare'
+ '/api/workplace_search/account/sources/123/reauth_prepare',
+ {
+ query: {
+ kibana_host: '',
+ },
+ }
);
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts
index ed63f82764f7e..6ca7f6fa72e24 100644
--- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts
@@ -418,8 +418,12 @@ export const AddSourceLogic = kea>;
+ preconfiguration_id?: string; // Uniqifies preconfigured policies by something other than `name`
}
export interface AgentPolicy extends NewAgentPolicy {
diff --git a/x-pack/plugins/fleet/common/types/models/index.ts b/x-pack/plugins/fleet/common/types/models/index.ts
index 89052a646ebf4..ded6b3d9ee59d 100644
--- a/x-pack/plugins/fleet/common/types/models/index.ts
+++ b/x-pack/plugins/fleet/common/types/models/index.ts
@@ -14,3 +14,4 @@ export * from './epm';
export * from './package_spec';
export * from './enrollment_api_key';
export * from './settings';
+export * from './preconfiguration';
diff --git a/x-pack/plugins/fleet/common/types/models/preconfiguration.ts b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts
new file mode 100644
index 0000000000000..b16234d5a5f97
--- /dev/null
+++ b/x-pack/plugins/fleet/common/types/models/preconfiguration.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type {
+ PackagePolicyPackage,
+ NewPackagePolicy,
+ NewPackagePolicyInput,
+} from './package_policy';
+import type { NewAgentPolicy } from './agent_policy';
+
+export type InputsOverride = Partial & {
+ vars?: Array;
+};
+
+export interface PreconfiguredAgentPolicy extends Omit {
+ id: string | number;
+ namespace?: string;
+ package_policies: Array<
+ Partial> & {
+ name: string;
+ package: Partial;
+ inputs?: InputsOverride[];
+ }
+ >;
+}
diff --git a/x-pack/plugins/fleet/server/constants/index.ts b/x-pack/plugins/fleet/server/constants/index.ts
index 23df18d5e377d..7f5586fb0f034 100644
--- a/x-pack/plugins/fleet/server/constants/index.ts
+++ b/x-pack/plugins/fleet/server/constants/index.ts
@@ -33,6 +33,7 @@ export {
SETUP_API_ROUTE,
SETTINGS_API_ROUTES,
APP_API_ROUTES,
+ PRECONFIGURATION_API_ROUTES,
// Saved object types
SO_SEARCH_LIMIT,
AGENT_SAVED_OBJECT_TYPE,
diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts
index 1d4e9e63d7a62..20cfae6bc1cf2 100644
--- a/x-pack/plugins/fleet/server/plugin.ts
+++ b/x-pack/plugins/fleet/server/plugin.ts
@@ -64,6 +64,7 @@ import {
registerOutputRoutes,
registerSettingsRoutes,
registerAppRoutes,
+ registerPreconfigurationRoutes,
} from './routes';
import type {
ESIndexPatternService,
@@ -274,6 +275,7 @@ export class FleetPlugin
registerSettingsRoutes(routerSuperuserOnly);
registerDataStreamRoutes(routerSuperuserOnly);
registerEPMRoutes(routerSuperuserOnly);
+ registerPreconfigurationRoutes(routerSuperuserOnly);
// Conditional config routes
if (config.agents.enabled) {
diff --git a/x-pack/plugins/fleet/server/routes/index.ts b/x-pack/plugins/fleet/server/routes/index.ts
index a98ea50fd1841..4d5a4b1e64dc0 100644
--- a/x-pack/plugins/fleet/server/routes/index.ts
+++ b/x-pack/plugins/fleet/server/routes/index.ts
@@ -20,3 +20,4 @@ export { registerRoutes as registerOutputRoutes } from './output';
export { registerRoutes as registerSettingsRoutes } from './settings';
export { registerRoutes as registerAppRoutes } from './app';
export { registerLimitedConcurrencyRoutes } from './limited_concurrency';
+export { registerRoutes as registerPreconfigurationRoutes } from './preconfiguration';
diff --git a/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
new file mode 100644
index 0000000000000..77fe74fda54d9
--- /dev/null
+++ b/x-pack/plugins/fleet/server/routes/preconfiguration/index.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { IRouter, RequestHandler } from 'src/core/server';
+import type { TypeOf } from '@kbn/config-schema';
+
+import type { PreconfiguredAgentPolicy } from '../../../common';
+
+import { PLUGIN_ID, PRECONFIGURATION_API_ROUTES } from '../../constants';
+import { PutPreconfigurationSchema } from '../../types';
+import { defaultIngestErrorHandler } from '../../errors';
+import { ensurePreconfiguredPackagesAndPolicies, outputService } from '../../services';
+
+export const putPreconfigurationHandler: RequestHandler<
+ undefined,
+ undefined,
+ TypeOf
+> = async (context, request, response) => {
+ const soClient = context.core.savedObjects.client;
+ const esClient = context.core.elasticsearch.client.asCurrentUser;
+ const defaultOutput = await outputService.ensureDefaultOutput(soClient);
+
+ const { agentPolicies, packages } = request.body;
+
+ try {
+ const body = await ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ (agentPolicies as PreconfiguredAgentPolicy[]) ?? [],
+ packages ?? [],
+ defaultOutput
+ );
+ return response.ok({ body });
+ } catch (error) {
+ return defaultIngestErrorHandler({ error, response });
+ }
+};
+
+export const registerRoutes = (router: IRouter) => {
+ router.put(
+ {
+ path: PRECONFIGURATION_API_ROUTES.PUT_PRECONFIG,
+ validate: PutPreconfigurationSchema,
+ options: { tags: [`access:${PLUGIN_ID}-all`] },
+ },
+ putPreconfigurationHandler
+ );
+};
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index 87ca9782ab698..8554c0702f733 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -178,6 +178,7 @@ const getSavedObjectTypes = (
updated_by: { type: 'keyword' },
revision: { type: 'integer' },
monitoring_enabled: { type: 'keyword', index: false },
+ preconfiguration_id: { type: 'keyword' },
},
},
migrations: {
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 357b9150407ef..f2e7453425865 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -5,7 +5,7 @@
* 2.0.
*/
-import { uniq } from 'lodash';
+import { uniq, omit } from 'lodash';
import { safeLoad } from 'js-yaml';
import uuid from 'uuid/v4';
import type {
@@ -23,19 +23,28 @@ import {
import type {
PackagePolicy,
NewAgentPolicy,
+ PreconfiguredAgentPolicy,
AgentPolicy,
AgentPolicySOAttributes,
FullAgentPolicy,
ListWithKuery,
+ NewPackagePolicy,
} from '../types';
import {
agentPolicyStatuses,
storedPackagePoliciesToAgentInputs,
dataTypes,
+ packageToPackagePolicy,
AGENT_POLICY_INDEX,
DEFAULT_FLEET_SERVER_AGENT_POLICY,
} from '../../common';
-import type { DeleteAgentPolicyResponse, Settings, FleetServerPolicy } from '../../common';
+import type {
+ DeleteAgentPolicyResponse,
+ Settings,
+ FleetServerPolicy,
+ Installation,
+ Output,
+} from '../../common';
import {
AgentPolicyNameExistsError,
AgentPolicyDeletionError,
@@ -43,6 +52,7 @@ import {
} from '../errors';
import { getFullAgentPolicyKibanaConfig } from '../../common/services/full_agent_policy_kibana_config';
+import { getPackageInfo } from './epm/packages';
import { createAgentPolicyAction, getAgentsByKuery } from './agents';
import { packagePolicyService } from './package_policy';
import { outputService } from './output';
@@ -106,55 +116,84 @@ class AgentPolicyService {
esClient: ElasticsearchClient
): Promise<{
created: boolean;
- defaultAgentPolicy: AgentPolicy;
+ policy: AgentPolicy;
}> {
- const agentPolicies = await soClient.find({
- type: AGENT_POLICY_SAVED_OBJECT_TYPE,
+ const searchParams = {
searchFields: ['is_default'],
search: 'true',
- });
+ };
+ return await this.ensureAgentPolicy(soClient, esClient, DEFAULT_AGENT_POLICY, searchParams);
+ }
- if (agentPolicies.total === 0) {
- const newDefaultAgentPolicy: NewAgentPolicy = {
- ...DEFAULT_AGENT_POLICY,
- };
+ public async ensureDefaultFleetServerAgentPolicy(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient
+ ): Promise<{
+ created: boolean;
+ policy: AgentPolicy;
+ }> {
+ const searchParams = {
+ searchFields: ['is_default_fleet_server'],
+ search: 'true',
+ };
+ return await this.ensureAgentPolicy(
+ soClient,
+ esClient,
+ DEFAULT_FLEET_SERVER_AGENT_POLICY,
+ searchParams
+ );
+ }
- return {
- created: true,
- defaultAgentPolicy: await this.create(soClient, esClient, newDefaultAgentPolicy),
- };
- }
+ public async ensurePreconfiguredAgentPolicy(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ config: PreconfiguredAgentPolicy
+ ): Promise<{
+ created: boolean;
+ policy: AgentPolicy;
+ }> {
+ const { id, ...preconfiguredAgentPolicy } = omit(config, 'package_policies');
+ const preconfigurationId = String(id);
+ const searchParams = {
+ searchFields: ['preconfiguration_id'],
+ search: escapeSearchQueryPhrase(preconfigurationId),
+ };
- return {
- created: false,
- defaultAgentPolicy: {
- id: agentPolicies.saved_objects[0].id,
- ...agentPolicies.saved_objects[0].attributes,
- },
+ const newAgentPolicyDefaults: Partial = {
+ namespace: 'default',
+ monitoring_enabled: ['logs', 'metrics'],
};
+
+ const newAgentPolicy = {
+ ...newAgentPolicyDefaults,
+ ...preconfiguredAgentPolicy,
+ preconfiguration_id: preconfigurationId,
+ } as NewAgentPolicy;
+
+ return await this.ensureAgentPolicy(soClient, esClient, newAgentPolicy, searchParams);
}
- public async ensureDefaultFleetServerAgentPolicy(
+ private async ensureAgentPolicy(
soClient: SavedObjectsClientContract,
- esClient: ElasticsearchClient
+ esClient: ElasticsearchClient,
+ newAgentPolicy: NewAgentPolicy,
+ searchParams: {
+ searchFields: string[];
+ search: string;
+ }
): Promise<{
created: boolean;
policy: AgentPolicy;
}> {
const agentPolicies = await soClient.find({
type: AGENT_POLICY_SAVED_OBJECT_TYPE,
- searchFields: ['is_default_fleet_server'],
- search: 'true',
+ ...searchParams,
});
if (agentPolicies.total === 0) {
- const newDefaultAgentPolicy: NewAgentPolicy = {
- ...DEFAULT_FLEET_SERVER_AGENT_POLICY,
- };
-
return {
created: true,
- policy: await this.create(soClient, esClient, newDefaultAgentPolicy),
+ policy: await this.create(soClient, esClient, newAgentPolicy),
};
}
@@ -514,7 +553,7 @@ class AgentPolicyService {
}
const {
- defaultAgentPolicy: { id: defaultAgentPolicyId },
+ policy: { id: defaultAgentPolicyId },
} = await this.ensureDefaultAgentPolicy(soClient, esClient);
if (id === defaultAgentPolicyId) {
throw new Error('The default agent policy cannot be deleted');
@@ -726,3 +765,37 @@ class AgentPolicyService {
}
export const agentPolicyService = new AgentPolicyService();
+
+export async function addPackageToAgentPolicy(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ packageToInstall: Installation,
+ agentPolicy: AgentPolicy,
+ defaultOutput: Output,
+ packagePolicyName?: string,
+ packagePolicyDescription?: string,
+ transformPackagePolicy?: (p: NewPackagePolicy) => NewPackagePolicy
+) {
+ const packageInfo = await getPackageInfo({
+ savedObjectsClient: soClient,
+ pkgName: packageToInstall.name,
+ pkgVersion: packageToInstall.version,
+ });
+
+ const basePackagePolicy = packageToPackagePolicy(
+ packageInfo,
+ agentPolicy.id,
+ defaultOutput.id,
+ agentPolicy.namespace ?? 'default',
+ packagePolicyName,
+ packagePolicyDescription
+ );
+
+ const newPackagePolicy = transformPackagePolicy
+ ? transformPackagePolicy(basePackagePolicy)
+ : basePackagePolicy;
+
+ await packagePolicyService.create(soClient, esClient, newPackagePolicy, {
+ bumpRevision: false,
+ });
+}
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
index 1a6b41976af98..7095bb1688c73 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
@@ -97,22 +97,54 @@ export async function ensureInstalledDefaultPackages(
});
}
+export async function isPackageVersionInstalled(options: {
+ savedObjectsClient: SavedObjectsClientContract;
+ pkgName: string;
+ pkgVersion?: string;
+}): Promise {
+ const { savedObjectsClient, pkgName, pkgVersion } = options;
+ const installedPackage = await getInstallation({ savedObjectsClient, pkgName });
+ if (installedPackage && (!pkgVersion || installedPackage.version === pkgVersion)) {
+ return installedPackage;
+ }
+ return false;
+}
+
export async function ensureInstalledPackage(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
esClient: ElasticsearchClient;
+ pkgVersion?: string;
}): Promise {
- const { savedObjectsClient, pkgName, esClient } = options;
- const installedPackage = await getInstallation({ savedObjectsClient, pkgName });
+ const { savedObjectsClient, pkgName, esClient, pkgVersion } = options;
+ const installedPackage = await isPackageVersionInstalled({
+ savedObjectsClient,
+ pkgName,
+ pkgVersion,
+ });
if (installedPackage) {
return installedPackage;
}
// if the requested packaged was not found to be installed, install
- await installLatestPackage({
- savedObjectsClient,
- pkgName,
- esClient,
- });
+ if (pkgVersion) {
+ const pkgkey = Registry.pkgToPkgKey({
+ name: pkgName,
+ version: pkgVersion,
+ });
+ await installPackage({
+ installSource: 'registry',
+ savedObjectsClient,
+ pkgkey,
+ esClient,
+ force: true,
+ });
+ } else {
+ await installLatestPackage({
+ savedObjectsClient,
+ pkgName,
+ esClient,
+ });
+ }
const installation = await getInstallation({ savedObjectsClient, pkgName });
if (!installation) throw new Error(`could not get installation ${pkgName}`);
return installation;
diff --git a/x-pack/plugins/fleet/server/services/index.ts b/x-pack/plugins/fleet/server/services/index.ts
index e9a8024a032e5..ebddb695d695b 100644
--- a/x-pack/plugins/fleet/server/services/index.ts
+++ b/x-pack/plugins/fleet/server/services/index.ts
@@ -83,3 +83,6 @@ export { licenseService } from './license';
// Artifacts services
export * from './artifacts';
+
+// Policy preconfiguration functions
+export { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration';
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts
new file mode 100644
index 0000000000000..bcde8ade427e5
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts
@@ -0,0 +1,244 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks';
+
+import type { PreconfiguredAgentPolicy } from '../../common/types';
+import type { AgentPolicy, NewPackagePolicy, Output } from '../types';
+
+import { ensurePreconfiguredPackagesAndPolicies } from './preconfiguration';
+
+const mockInstalledPackages = new Map();
+const mockConfiguredPolicies = new Map();
+
+const mockDefaultOutput: Output = {
+ id: 'test-id',
+ is_default: true,
+ name: 'default',
+ // @ts-ignore
+ type: 'elasticsearch',
+ hosts: ['http://127.0.0.1:9201'],
+};
+
+function getPutPreconfiguredPackagesMock() {
+ const soClient = savedObjectsClientMock.create();
+ soClient.find.mockImplementation(async ({ type, search }) => {
+ const attributes = mockConfiguredPolicies.get(search!.replace(/"/g, ''));
+ if (attributes) {
+ return {
+ saved_objects: [
+ {
+ id: `mocked-${attributes.preconfiguration_id}`,
+ attributes,
+ type: type as string,
+ score: 1,
+ references: [],
+ },
+ ],
+ total: 1,
+ page: 1,
+ per_page: 1,
+ };
+ } else {
+ return {
+ saved_objects: [],
+ total: 0,
+ page: 1,
+ per_page: 0,
+ };
+ }
+ });
+ soClient.create.mockImplementation(async (type, policy) => {
+ const attributes = policy as AgentPolicy;
+ mockConfiguredPolicies.set(attributes.preconfiguration_id, attributes);
+ return {
+ id: `mocked-${attributes.preconfiguration_id}`,
+ attributes,
+ type,
+ references: [],
+ };
+ });
+ return soClient;
+}
+
+jest.mock('./epm/packages/install', () => ({
+ ensureInstalledPackage({ pkgName, pkgVersion }: { pkgName: string; pkgVersion: string }) {
+ const installedPackage = mockInstalledPackages.get(pkgName);
+ if (installedPackage) return installedPackage;
+
+ const packageInstallation = { name: pkgName, version: pkgVersion, title: pkgName };
+ mockInstalledPackages.set(pkgName, packageInstallation);
+ return packageInstallation;
+ },
+}));
+
+jest.mock('./epm/packages/get', () => ({
+ getPackageInfo({ pkgName }: { pkgName: string }) {
+ const installedPackage = mockInstalledPackages.get(pkgName);
+ if (!installedPackage) return { status: 'not_installed' };
+ return {
+ status: 'installed',
+ ...installedPackage,
+ };
+ },
+ getInstallation({ pkgName }: { pkgName: string }) {
+ return mockInstalledPackages.get(pkgName) ?? false;
+ },
+}));
+
+jest.mock('./package_policy', () => ({
+ packagePolicyService: {
+ create(soClient: any, esClient: any, newPackagePolicy: NewPackagePolicy) {
+ return {
+ id: 'mocked',
+ version: 'mocked',
+ ...newPackagePolicy,
+ };
+ },
+ },
+}));
+
+jest.mock('./agents/setup', () => ({
+ isAgentsSetup() {
+ return false;
+ },
+}));
+
+describe('policy preconfiguration', () => {
+ beforeEach(() => {
+ mockInstalledPackages.clear();
+ mockConfiguredPolicies.clear();
+ });
+
+ it('should perform a no-op when passed no policies or packages', async () => {
+ const soClient = getPutPreconfiguredPackagesMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ const { policies, packages } = await ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ [],
+ [],
+ mockDefaultOutput
+ );
+
+ expect(policies.length).toBe(0);
+ expect(packages.length).toBe(0);
+ });
+
+ it('should install packages successfully', async () => {
+ const soClient = getPutPreconfiguredPackagesMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ const { policies, packages } = await ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ [],
+ [{ name: 'test-package', version: '3.0.0' }],
+ mockDefaultOutput
+ );
+
+ expect(policies.length).toBe(0);
+ expect(packages).toEqual(expect.arrayContaining(['test-package:3.0.0']));
+ });
+
+ it('should install packages and configure agent policies successfully', async () => {
+ const soClient = getPutPreconfiguredPackagesMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ const { policies, packages } = await ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ [
+ {
+ name: 'Test policy',
+ namespace: 'default',
+ id: 'test-id',
+ package_policies: [
+ {
+ package: { name: 'test-package' },
+ name: 'Test package',
+ },
+ ],
+ },
+ ] as PreconfiguredAgentPolicy[],
+ [{ name: 'test-package', version: '3.0.0' }],
+ mockDefaultOutput
+ );
+
+ expect(policies.length).toEqual(1);
+ expect(policies[0].id).toBe('mocked-test-id');
+ expect(packages).toEqual(expect.arrayContaining(['test-package:3.0.0']));
+ });
+
+ it('should throw an error when trying to install duplicate packages', async () => {
+ const soClient = getPutPreconfiguredPackagesMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ await expect(
+ ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ [],
+ [
+ { name: 'test-package', version: '3.0.0' },
+ { name: 'test-package', version: '2.0.0' },
+ ],
+ mockDefaultOutput
+ )
+ ).rejects.toThrow(
+ 'Duplicate packages specified in configuration: test-package:3.0.0, test-package:2.0.0'
+ );
+ });
+
+ it('should not attempt to recreate or modify an agent policy if its ID is unchanged', async () => {
+ const soClient = getPutPreconfiguredPackagesMock();
+ const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
+
+ const { policies: policiesA } = await ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ [
+ {
+ name: 'Test policy',
+ namespace: 'default',
+ id: 'test-id',
+ package_policies: [],
+ },
+ ] as PreconfiguredAgentPolicy[],
+ [],
+ mockDefaultOutput
+ );
+
+ expect(policiesA.length).toEqual(1);
+ expect(policiesA[0].id).toBe('mocked-test-id');
+
+ const { policies: policiesB } = await ensurePreconfiguredPackagesAndPolicies(
+ soClient,
+ esClient,
+ [
+ {
+ name: 'Test policy redo',
+ namespace: 'default',
+ id: 'test-id',
+ package_policies: [
+ {
+ package: { name: 'some-uninstalled-package' },
+ name: 'This package is not installed',
+ },
+ ],
+ },
+ ] as PreconfiguredAgentPolicy[],
+ [],
+ mockDefaultOutput
+ );
+
+ expect(policiesB.length).toEqual(1);
+ expect(policiesB[0].id).toBe('mocked-test-id');
+ expect(policiesB[0].updated_at).toEqual(policiesA[0].updated_at);
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts
new file mode 100644
index 0000000000000..bd1c2ca1f23ef
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts
@@ -0,0 +1,272 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
+import { i18n } from '@kbn/i18n';
+import { groupBy } from 'lodash';
+
+import type {
+ PackagePolicyPackage,
+ NewPackagePolicy,
+ AgentPolicy,
+ Installation,
+ Output,
+ NewPackagePolicyInput,
+ NewPackagePolicyInputStream,
+ PreconfiguredAgentPolicy,
+} from '../../common';
+
+import { getInstallation } from './epm/packages';
+import { ensureInstalledPackage } from './epm/packages/install';
+import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy';
+
+export type InputsOverride = Partial & {
+ vars?: Array;
+};
+
+export async function ensurePreconfiguredPackagesAndPolicies(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ policies: PreconfiguredAgentPolicy[] = [],
+ packages: Array> = [],
+ defaultOutput: Output
+) {
+ // Validate configured packages to ensure there are no version conflicts
+ const packageNames = groupBy(packages, (pkg) => pkg.name);
+ const duplicatePackages = Object.entries(packageNames).filter(
+ ([, versions]) => versions.length > 1
+ );
+ if (duplicatePackages.length) {
+ // List duplicate packages as a comma-separated list of :
+ // If there are multiple packages with duplicate versions, separate them with semicolons, e.g
+ // package-a:1.0.0, package-a:2.0.0; package-b:1.0.0, package-b:2.0.0
+ const duplicateList = duplicatePackages
+ .map(([, versions]) => versions.map((v) => `${v.name}:${v.version}`).join(', '))
+ .join('; ');
+
+ throw new Error(
+ i18n.translate('xpack.fleet.preconfiguration.duplicatePackageError', {
+ defaultMessage: 'Duplicate packages specified in configuration: {duplicateList}',
+ values: {
+ duplicateList,
+ },
+ })
+ );
+ }
+
+ // Preinstall packages specified in Kibana config
+ const preconfiguredPackages = await Promise.all(
+ packages.map(({ name, version }) =>
+ ensureInstalledPreconfiguredPackage(soClient, esClient, name, version)
+ )
+ );
+
+ // Create policies specified in Kibana config
+ const preconfiguredPolicies = await Promise.all(
+ policies.map(async (preconfiguredAgentPolicy) => {
+ const { created, policy } = await agentPolicyService.ensurePreconfiguredAgentPolicy(
+ soClient,
+ esClient,
+ preconfiguredAgentPolicy
+ );
+
+ if (!created) return { created, policy };
+ const { package_policies: packagePolicies } = preconfiguredAgentPolicy;
+
+ const installedPackagePolicies = await Promise.all(
+ packagePolicies.map(async ({ package: pkg, name, ...newPackagePolicy }) => {
+ const installedPackage = await getInstallation({
+ savedObjectsClient: soClient,
+ pkgName: pkg.name,
+ });
+ if (!installedPackage) {
+ throw new Error(
+ i18n.translate('xpack.fleet.preconfiguration.packageMissingError', {
+ defaultMessage:
+ '{agentPolicyName} could not be added. {pkgName} is not installed, add {pkgName} to `{packagesConfigValue}` or remove it from {packagePolicyName}.',
+ values: {
+ agentPolicyName: preconfiguredAgentPolicy.name,
+ packagePolicyName: name,
+ pkgName: pkg.name,
+ packagesConfigValue: 'xpack.fleet.packages',
+ },
+ })
+ );
+ }
+ return { name, installedPackage, ...newPackagePolicy };
+ })
+ );
+
+ return { created, policy, installedPackagePolicies };
+ })
+ );
+
+ for (const preconfiguredPolicy of preconfiguredPolicies) {
+ const { created, policy, installedPackagePolicies } = preconfiguredPolicy;
+ if (created) {
+ await addPreconfiguredPolicyPackages(
+ soClient,
+ esClient,
+ policy,
+ installedPackagePolicies!,
+ defaultOutput
+ );
+ }
+ }
+
+ return {
+ policies: preconfiguredPolicies.map((p) => ({
+ id: p.policy.id,
+ updated_at: p.policy.updated_at,
+ })),
+ packages: preconfiguredPackages.map((pkg) => `${pkg.name}:${pkg.version}`),
+ };
+}
+
+async function addPreconfiguredPolicyPackages(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ agentPolicy: AgentPolicy,
+ installedPackagePolicies: Array<
+ Partial> & {
+ name: string;
+ installedPackage: Installation;
+ inputs?: InputsOverride[];
+ }
+ >,
+ defaultOutput: Output
+) {
+ return await Promise.all(
+ installedPackagePolicies.map(async ({ installedPackage, name, description, inputs }) =>
+ addPackageToAgentPolicy(
+ soClient,
+ esClient,
+ installedPackage,
+ agentPolicy,
+ defaultOutput,
+ name,
+ description,
+ (policy) => overridePackageInputs(policy, inputs)
+ )
+ )
+ );
+}
+
+async function ensureInstalledPreconfiguredPackage(
+ soClient: SavedObjectsClientContract,
+ esClient: ElasticsearchClient,
+ pkgName: string,
+ pkgVersion: string
+) {
+ return ensureInstalledPackage({
+ savedObjectsClient: soClient,
+ pkgName,
+ esClient,
+ pkgVersion,
+ });
+}
+
+function overridePackageInputs(
+ basePackagePolicy: NewPackagePolicy,
+ inputsOverride?: InputsOverride[]
+) {
+ if (!inputsOverride) return basePackagePolicy;
+
+ const inputs = [...basePackagePolicy.inputs];
+ const packageName = basePackagePolicy.package!.name;
+
+ for (const override of inputsOverride) {
+ const originalInput = inputs.find((i) => i.type === override.type);
+ if (!originalInput) {
+ throw new Error(
+ i18n.translate('xpack.fleet.packagePolicyInputOverrideError', {
+ defaultMessage: 'Input type {inputType} does not exist on package {packageName}',
+ values: {
+ inputType: override.type,
+ packageName,
+ },
+ })
+ );
+ }
+
+ if (typeof override.enabled !== 'undefined') originalInput.enabled = override.enabled;
+
+ if (override.vars) {
+ try {
+ deepMergeVars(override, originalInput);
+ } catch (e) {
+ throw new Error(
+ i18n.translate('xpack.fleet.packagePolicyVarOverrideError', {
+ defaultMessage: 'Var {varName} does not exist on {inputType} of package {packageName}',
+ values: {
+ varName: e.message,
+ inputType: override.type,
+ packageName,
+ },
+ })
+ );
+ }
+ }
+
+ if (override.streams) {
+ for (const stream of override.streams) {
+ const originalStream = originalInput.streams.find(
+ (s) => s.data_stream.dataset === stream.data_stream.dataset
+ );
+ if (!originalStream) {
+ throw new Error(
+ i18n.translate('xpack.fleet.packagePolicyStreamOverrideError', {
+ defaultMessage:
+ 'Data stream {streamSet} does not exist on {inputType} of package {packageName}',
+ values: {
+ streamSet: stream.data_stream.dataset,
+ inputType: override.type,
+ packageName,
+ },
+ })
+ );
+ }
+
+ if (typeof stream.enabled !== 'undefined') originalStream.enabled = stream.enabled;
+
+ if (stream.vars) {
+ try {
+ deepMergeVars(stream as InputsOverride, originalStream);
+ } catch (e) {
+ throw new Error(
+ i18n.translate('xpack.fleet.packagePolicyStreamVarOverrideError', {
+ defaultMessage:
+ 'Var {varName} does not exist on {streamSet} for {inputType} of package {packageName}',
+ values: {
+ varName: e.message,
+ streamSet: stream.data_stream.dataset,
+ inputType: override.type,
+ packageName,
+ },
+ })
+ );
+ }
+ }
+ }
+ }
+ }
+
+ return { ...basePackagePolicy, inputs };
+}
+
+function deepMergeVars(
+ override: InputsOverride,
+ original: NewPackagePolicyInput | NewPackagePolicyInputStream
+) {
+ for (const { name, ...val } of override.vars!) {
+ if (!original.vars || !Reflect.has(original.vars, name)) {
+ throw new Error(name);
+ }
+ const originalVar = original.vars[name];
+ Reflect.set(original.vars, name, { ...originalVar, ...val });
+ }
+}
diff --git a/x-pack/plugins/fleet/server/services/setup.ts b/x-pack/plugins/fleet/server/services/setup.ts
index 6f4ca6e231e9e..b5e2326386e02 100644
--- a/x-pack/plugins/fleet/server/services/setup.ts
+++ b/x-pack/plugins/fleet/server/services/setup.ts
@@ -7,26 +7,22 @@
import uuid from 'uuid';
import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server';
+import { i18n } from '@kbn/i18n';
-import {
- packageToPackagePolicy,
- DEFAULT_AGENT_POLICIES_PACKAGES,
- FLEET_SERVER_PACKAGE,
-} from '../../common';
+import { DEFAULT_AGENT_POLICIES_PACKAGES, FLEET_SERVER_PACKAGE } from '../../common';
-import type { PackagePolicy, AgentPolicy, Installation, Output } from '../../common';
+import type { PackagePolicy } from '../../common';
import { SO_SEARCH_LIMIT } from '../constants';
-import { agentPolicyService } from './agent_policy';
+import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy';
import { outputService } from './output';
import {
ensureInstalledDefaultPackages,
ensureInstalledPackage,
ensurePackagesCompletedInstall,
} from './epm/packages/install';
-import { getPackageInfo } from './epm/packages';
-import { packagePolicyService } from './package_policy';
+
import { generateEnrollmentAPIKey } from './api_keys';
import { settingsService } from '.';
import { awaitIfPending } from './setup_utils';
@@ -55,7 +51,7 @@ async function createSetupSideEffects(
const [
installedPackages,
defaultOutput,
- { created: defaultAgentPolicyCreated, defaultAgentPolicy },
+ { created: defaultAgentPolicyCreated, policy: defaultAgentPolicy },
{ created: defaultFleetServerPolicyCreated, policy: defaultFleetServerPolicy },
] = await Promise.all([
// packages installed by default
@@ -110,13 +106,21 @@ async function createSetupSideEffects(
true
);
if (!agentPolicyWithPackagePolicies) {
- throw new Error('Policy not found');
+ throw new Error(
+ i18n.translate('xpack.fleet.setup.policyNotFoundError', {
+ defaultMessage: 'Policy not found',
+ })
+ );
}
if (
agentPolicyWithPackagePolicies.package_policies.length &&
typeof agentPolicyWithPackagePolicies.package_policies[0] === 'string'
) {
- throw new Error('Policy not found');
+ throw new Error(
+ i18n.translate('xpack.fleet.setup.policyNotFoundError', {
+ defaultMessage: 'Policy not found',
+ })
+ );
}
for (const installedPackage of installedPackages) {
@@ -210,7 +214,11 @@ export async function setupFleet(
// save fleet admin user
const defaultOutputId = await outputService.getDefaultOutputId(soClient);
if (!defaultOutputId) {
- throw new Error('Default output does not exist');
+ throw new Error(
+ i18n.translate('xpack.fleet.setup.defaultOutputError', {
+ defaultMessage: 'Default output does not exist',
+ })
+ );
}
await outputService.updateOutput(soClient, defaultOutputId, {
@@ -242,28 +250,3 @@ export async function setupFleet(
function generateRandomPassword() {
return Buffer.from(uuid.v4()).toString('base64');
}
-
-async function addPackageToAgentPolicy(
- soClient: SavedObjectsClientContract,
- esClient: ElasticsearchClient,
- packageToInstall: Installation,
- agentPolicy: AgentPolicy,
- defaultOutput: Output
-) {
- const packageInfo = await getPackageInfo({
- savedObjectsClient: soClient,
- pkgName: packageToInstall.name,
- pkgVersion: packageToInstall.version,
- });
-
- const newPackagePolicy = packageToPackagePolicy(
- packageInfo,
- agentPolicy.id,
- defaultOutput.id,
- agentPolicy.namespace
- );
-
- await packagePolicyService.create(soClient, esClient, newPackagePolicy, {
- bumpRevision: false,
- });
-}
diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx
index 2b46f7e76a719..581a8241f09bf 100644
--- a/x-pack/plugins/fleet/server/types/index.tsx
+++ b/x-pack/plugins/fleet/server/types/index.tsx
@@ -32,6 +32,7 @@ export {
AgentPolicy,
AgentPolicySOAttributes,
NewAgentPolicy,
+ PreconfiguredAgentPolicy,
AgentPolicyStatus,
DataStream,
Output,
diff --git a/x-pack/plugins/fleet/server/types/models/agent_policy.ts b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
index 90615c2df7bf6..db551b25e9ebb 100644
--- a/x-pack/plugins/fleet/server/types/models/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/types/models/agent_policy.ts
@@ -11,7 +11,7 @@ import { agentPolicyStatuses, dataTypes } from '../../../common';
import { PackagePolicySchema, NamespaceSchema } from './package_policy';
-const AgentPolicyBaseSchema = {
+export const AgentPolicyBaseSchema = {
name: schema.string({ minLength: 1 }),
namespace: NamespaceSchema,
description: schema.maybe(schema.string()),
diff --git a/x-pack/plugins/fleet/server/types/models/index.ts b/x-pack/plugins/fleet/server/types/models/index.ts
index bac4e7061a9ab..d71c6b2e0d748 100644
--- a/x-pack/plugins/fleet/server/types/models/index.ts
+++ b/x-pack/plugins/fleet/server/types/models/index.ts
@@ -10,3 +10,4 @@ export * from './agent';
export * from './package_policy';
export * from './output';
export * from './enrollment_api_key';
+export * from './preconfiguration';
diff --git a/x-pack/plugins/fleet/server/types/models/preconfiguration.ts b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts
new file mode 100644
index 0000000000000..77a28defaf1bd
--- /dev/null
+++ b/x-pack/plugins/fleet/server/types/models/preconfiguration.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { i18n } from '@kbn/i18n';
+import { schema } from '@kbn/config-schema';
+import semverValid from 'semver/functions/valid';
+
+import { AgentPolicyBaseSchema } from './agent_policy';
+import { NamespaceSchema } from './package_policy';
+
+const varsSchema = schema.maybe(
+ schema.arrayOf(
+ schema.object({
+ name: schema.string(),
+ type: schema.maybe(schema.string()),
+ value: schema.oneOf([schema.string(), schema.number()]),
+ })
+ )
+);
+
+export const PreconfiguredPackagesSchema = schema.arrayOf(
+ schema.object({
+ name: schema.string(),
+ version: schema.string({
+ validate: (value) => {
+ if (!semverValid(value)) {
+ return i18n.translate('xpack.fleet.config.invalidPackageVersionError', {
+ defaultMessage: 'must be a valid semver',
+ });
+ }
+ },
+ }),
+ })
+);
+
+export const PreconfiguredAgentPoliciesSchema = schema.arrayOf(
+ schema.object({
+ ...AgentPolicyBaseSchema,
+ namespace: schema.maybe(NamespaceSchema),
+ id: schema.oneOf([schema.string(), schema.number()]),
+ package_policies: schema.arrayOf(
+ schema.object({
+ name: schema.string(),
+ package: schema.object({
+ name: schema.string(),
+ }),
+ description: schema.maybe(schema.string()),
+ namespace: schema.maybe(NamespaceSchema),
+ inputs: schema.maybe(
+ schema.arrayOf(
+ schema.object({
+ type: schema.string(),
+ enabled: schema.maybe(schema.boolean()),
+ vars: varsSchema,
+ streams: schema.maybe(
+ schema.arrayOf(
+ schema.object({
+ data_stream: schema.object({
+ type: schema.maybe(schema.string()),
+ dataset: schema.string(),
+ }),
+ enabled: schema.maybe(schema.boolean()),
+ vars: varsSchema,
+ })
+ )
+ ),
+ })
+ )
+ ),
+ })
+ ),
+ })
+);
diff --git a/x-pack/plugins/fleet/server/types/rest_spec/index.ts b/x-pack/plugins/fleet/server/types/rest_spec/index.ts
index 3e4c465dfad66..c4fbb05ffd42f 100644
--- a/x-pack/plugins/fleet/server/types/rest_spec/index.ts
+++ b/x-pack/plugins/fleet/server/types/rest_spec/index.ts
@@ -13,5 +13,6 @@ export * from './epm';
export * from './enrollment_api_key';
export * from './install_script';
export * from './output';
+export * from './preconfiguration';
export * from './settings';
export * from './setup';
diff --git a/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts b/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts
new file mode 100644
index 0000000000000..dc802b89f1894
--- /dev/null
+++ b/x-pack/plugins/fleet/server/types/rest_spec/preconfiguration.ts
@@ -0,0 +1,17 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+
+import { PreconfiguredAgentPoliciesSchema, PreconfiguredPackagesSchema } from '../models';
+
+export const PutPreconfigurationSchema = {
+ body: schema.object({
+ agentPolicies: schema.maybe(PreconfiguredAgentPoliciesSchema),
+ packages: schema.maybe(PreconfiguredPackagesSchema),
+ }),
+};
diff --git a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx
index 16d58c8655383..b660bdcfdce8f 100644
--- a/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/ccr_read_exceptions_alert/index.tsx
@@ -42,7 +42,7 @@ export function createCCRReadExceptionsAlertType(): AlertTypeModel (
diff --git a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx
index 01b812568ff58..89139f12dacca 100644
--- a/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/large_shard_size_alert/index.tsx
@@ -42,7 +42,7 @@ export function createLargeShardSizeAlertType(): AlertTypeModel
description: ALERT_DETAILS[ALERT_LARGE_SHARD_SIZE].description,
iconClass: 'bell',
documentationUrl(docLinks) {
- return `${docLinks.links.monitoring.alertsKibana}`;
+ return `${docLinks.links.monitoring.alertsKibanaLargeShardSize}`;
},
alertParamsExpression: (props: Props) => (
diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx
index 87850f893797a..82bdf2786f89e 100644
--- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx
@@ -23,7 +23,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] {
description: LEGACY_ALERT_DETAILS[legacyAlert].description,
iconClass: 'bell',
documentationUrl(docLinks) {
- return `${docLinks.links.monitoring.alertsCluster}`;
+ return `${docLinks.links.monitoring.alertsKibanaClusterAlerts}`;
},
alertParamsExpression: () => (
diff --git a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx
index 00f560b602572..f3e697bd270e0 100644
--- a/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx
+++ b/x-pack/plugins/monitoring/public/alerts/thread_pool_rejections_alert/index.tsx
@@ -33,7 +33,7 @@ export function createThreadPoolRejectionsAlertType(
description: threadPoolAlertDetails.description,
iconClass: 'bell',
documentationUrl(docLinks) {
- return `${docLinks.links.monitoring.alertsKibana}`;
+ return `${docLinks.links.monitoring.alertsKibanaThreadpoolRejections}`;
},
alertParamsExpression: (props: Props) => (
<>
diff --git a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts
index 76eab7ab85cf1..542982778dfff 100644
--- a/x-pack/test/apm_api_integration/common/apm_api_supertest.ts
+++ b/x-pack/test/apm_api_integration/common/apm_api_supertest.ts
@@ -7,6 +7,7 @@
import { format } from 'url';
import supertest from 'supertest';
+import request from 'superagent';
import { MaybeParams } from '../../../plugins/apm/server/routes/typings';
import { parseEndpoint } from '../../../plugins/apm/common/apm_api/parse_endpoint';
import { APMAPI } from '../../../plugins/apm/server/routes/create_apm_api';
@@ -35,16 +36,24 @@ export function createApmApiSupertest(st: supertest.SuperTest) {
// supertest doesn't throw on http errors
if (res.status !== 200) {
- const e = new Error(
- `Unhandled ApmApiSupertest error. Status: "${
- res.status
- }". Endpoint: "${endpoint}". ${JSON.stringify(res.body)}`
- );
- // @ts-expect-error
- e.res = res;
- throw e;
+ throw new ApmApiError(res, endpoint);
}
return res;
};
}
+
+export class ApmApiError extends Error {
+ res: request.Response;
+
+ constructor(res: request.Response, endpoint: string) {
+ super(
+ `Unhandled ApmApiError.
+Status: "${res.status}"
+Endpoint: "${endpoint}"
+Body: ${JSON.stringify(res.body)}`
+ );
+
+ this.res = res;
+ }
+}
diff --git a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts
index fbd60c0f1ab1a..16c1cd92f2eae 100644
--- a/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts
+++ b/x-pack/test/apm_api_integration/tests/settings/agent_configuration.ts
@@ -103,41 +103,20 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte
describe('as a read-only user', () => {
const newConfig = { service: {}, settings: { transaction_sample_rate: '0.55' } };
- it('throws when attempting to create config', async () => {
- try {
- await createConfiguration(newConfig, { user: 'read' });
-
- // ensure that `createConfiguration` throws
- expect(true).to.be(false);
- } catch (e) {
- expect(e.res.statusCode).to.be(403);
- }
+ it('does not allow creating config', async () => {
+ await expectStatusCode(() => createConfiguration(newConfig, { user: 'read' }), 403);
});
describe('when a configuration already exists', () => {
before(async () => createConfiguration(newConfig));
after(async () => deleteConfiguration(newConfig));
- it('throws when attempting to update config', async () => {
- try {
- await updateConfiguration(newConfig, { user: 'read' });
-
- // ensure that `updateConfiguration` throws
- expect(true).to.be(false);
- } catch (e) {
- expect(e.res.statusCode).to.be(403);
- }
+ it('does not allow updating the config', async () => {
+ await expectStatusCode(() => updateConfiguration(newConfig, { user: 'read' }), 403);
});
- it('throws when attempting to delete config', async () => {
- try {
- await deleteConfiguration(newConfig, { user: 'read' });
-
- // ensure that line above throws
- expect(true).to.be(false);
- } catch (e) {
- expect(e.res.statusCode).to.be(403);
- }
+ it('does not allow deleting the config', async () => {
+ await expectStatusCode(() => deleteConfiguration(newConfig, { user: 'read' }), 403);
});
});
});
diff --git a/x-pack/test/apm_api_integration/tests/settings/custom_link.ts b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts
index c975a8219ddd3..7f1fb7df68390 100644
--- a/x-pack/test/apm_api_integration/tests/settings/custom_link.ts
+++ b/x-pack/test/apm_api_integration/tests/settings/custom_link.ts
@@ -9,7 +9,7 @@ import expect from '@kbn/expect';
import { CustomLink } from '../../../../plugins/apm/common/custom_link/custom_link_types';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { registry } from '../../common/registry';
-import { createApmApiSupertest } from '../../common/apm_api_supertest';
+import { ApmApiError, createApmApiSupertest } from '../../common/apm_api_supertest';
export default function customLinksTests({ getService }: FtrProviderContext) {
const supertestRead = createApmApiSupertest(getService('supertest'));
@@ -29,15 +29,11 @@ export default function customLinksTests({ getService }: FtrProviderContext) {
],
} as CustomLink;
- try {
- await createCustomLink(customLink);
- expect(true).to.be(false);
- } catch (e) {
- expect(e.res.status).to.be(403);
- expectSnapshot(e.res.body.message).toMatchInline(
- `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."`
- );
- }
+ const err = await expectToReject(() => createCustomLink(customLink));
+ expect(err.res.status).to.be(403);
+ expectSnapshot(err.res.body.message).toMatchInline(
+ `"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."`
+ );
});
});
@@ -184,3 +180,12 @@ export default function customLinksTests({ getService }: FtrProviderContext) {
});
}
}
+
+async function expectToReject(fn: () => Promise): Promise {
+ try {
+ await fn();
+ } catch (e) {
+ return e;
+ }
+ throw new Error(`Expected fn to throw`);
+}
diff --git a/x-pack/test/fleet_api_integration/apis/index.js b/x-pack/test/fleet_api_integration/apis/index.js
index 27987e469cfe7..ce5075e3e3b76 100644
--- a/x-pack/test/fleet_api_integration/apis/index.js
+++ b/x-pack/test/fleet_api_integration/apis/index.js
@@ -47,5 +47,8 @@ export default function ({ loadTestFile }) {
// Settings
loadTestFile(require.resolve('./settings/index'));
+
+ // Preconfiguration
+ loadTestFile(require.resolve('./preconfiguration/index'));
});
}
diff --git a/x-pack/test/fleet_api_integration/apis/preconfiguration/index.js b/x-pack/test/fleet_api_integration/apis/preconfiguration/index.js
new file mode 100644
index 0000000000000..9b97cda898a0e
--- /dev/null
+++ b/x-pack/test/fleet_api_integration/apis/preconfiguration/index.js
@@ -0,0 +1,12 @@
+/*
+ * 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.
+ */
+
+export default function loadTests({ loadTestFile }) {
+ describe('Preconfiguration Endpoints', () => {
+ loadTestFile(require.resolve('./preconfiguration'));
+ });
+}
diff --git a/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts
new file mode 100644
index 0000000000000..a6ae2d34ed8da
--- /dev/null
+++ b/x-pack/test/fleet_api_integration/apis/preconfiguration/preconfiguration.ts
@@ -0,0 +1,47 @@
+/*
+ * 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 { PRECONFIGURATION_API_ROUTES } from '../../../../plugins/fleet/common/constants';
+import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
+import { skipIfNoDockerRegistry } from '../../helpers';
+
+export default function (providerContext: FtrProviderContext) {
+ const { getService } = providerContext;
+ const supertest = getService('supertest');
+
+ // use function () {} and not () => {} here
+ // because `this` has to point to the Mocha context
+ // see https://mochajs.org/#arrow-functions
+
+ describe('Preconfiguration', async () => {
+ skipIfNoDockerRegistry(providerContext);
+ before(async () => {
+ await getService('esArchiver').load('empty_kibana');
+ await getService('esArchiver').load('fleet/empty_fleet_server');
+ });
+
+ after(async () => {
+ await getService('esArchiver').unload('fleet/empty_fleet_server');
+ await getService('esArchiver').unload('empty_kibana');
+ });
+
+ // Basic health check for the API; functionality is covered by the unit tests
+ it('should succeed with an empty payload', async () => {
+ const { body } = await supertest
+ .put(PRECONFIGURATION_API_ROUTES.PUT_PRECONFIG)
+ .set('kbn-xsrf', 'xxxx')
+ .send({})
+ .expect(200);
+
+ expect(body).to.eql({
+ packages: [],
+ policies: [],
+ });
+ });
+ });
+}