From 5431d1fb78011385813384ca57c5aebf124afaea Mon Sep 17 00:00:00 2001 From: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> Date: Wed, 2 Nov 2022 09:48:22 +0100 Subject: [PATCH] Prerelease toggle (#143853) * WIP: prerelease toggle * changed styling * fixed switch * auto upgrade and hiding button on callout if no ga available * tweak prereleaseIntegrationsEnabled state to add undefined state in beginning * fixing types and tests * fixing types * removed dummy endpoint package * extracted hooks to avoid double loading of packages and categories * prevent double loading of package details * updated openapi * fixing tests * added try catch around loading settings in preconfig * error handling on integrations, fixing cypress test with that * reading prerelease from settings during package install * fix tests * fixing tests * added back experimental as deprecated, fix more tests * fixed issue in package details overview where nlatest version didnt show prerelease * changed getPackageInfo to load prerelease from settings if not provided * fixing tests, moved getSettings to bulk install fn * fixing cypress and endpoint tests * fix tests * fix tests * added back experimental flag in other plugins, as it is not exaclty the same as prerelease * reverted mappings change in api_integration * fixed prerelease condition, fix limited test, trying to fix field limit * removed experimental flag from epr api call * added unit test on version dropdown and prerelease callout, set field limit to 1500 * added UI package version check for prerelease disabled case * fixed synthetics test * extracted getSettings to a helper function * removed using prerease setting in auto upgrades and install * fixing tests, added back prerelease flag to install apis * fixing a bug with version and release badge of installed integrations * reload package in overview after loading prerelease setting, this is to show available upgrade if package is installed * fixing tests by passing prerelease flag on apis * fixing cypress test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../migrations/check_registered_types.test.ts | 2 +- .../plugins/fleet/common/openapi/bundled.json | 58 +++++++- .../plugins/fleet/common/openapi/bundled.yaml | 40 +++++ .../openapi/components/schemas/settings.yaml | 2 + .../common/openapi/paths/epm@categories.yaml | 19 +++ .../common/openapi/paths/epm@packages.yaml | 17 +++ .../fleet/common/types/models/settings.ts | 1 + .../fleet/common/types/rest_spec/epm.ts | 4 + .../fleet/cypress/e2e/install_assets.cy.ts | 2 +- .../fleet/cypress/e2e/integrations_mock.cy.ts | 6 +- .../fleet/cypress/e2e/integrations_real.cy.ts | 2 +- .../debug/components/integration_debugger.tsx | 2 +- .../applications/integrations/hooks/index.ts | 2 + .../integrations/hooks/use_categories.tsx | 55 +++++++ .../integrations/hooks/use_packages.tsx | 56 +++++++ .../integration_preference.stories.tsx | 8 +- .../epm/components/integration_preference.tsx | 67 ++++++++- .../epm/screens/detail/index.test.tsx | 75 +++++++++- .../sections/epm/screens/detail/index.tsx | 116 ++++++++++++++- .../epm/screens/detail/overview/overview.tsx | 140 ++++++++++++------ .../epm/screens/home/available_packages.tsx | 31 ++-- .../sections/epm/screens/home/index.tsx | 25 +++- .../hooks/use_package_installations.tsx | 2 +- .../fleet/public/hooks/use_request/epm.ts | 35 ++++- x-pack/plugins/fleet/public/services/index.ts | 1 + .../services/package_prerelease.test.ts | 34 +++++ .../public/services/package_prerelease.ts | 11 ++ .../install_all_packages.ts | 2 +- x-pack/plugins/fleet/server/plugin.ts | 2 +- .../fleet/server/routes/epm/handlers.ts | 22 ++- .../server/routes/package_policy/handlers.ts | 1 + .../fleet/server/saved_objects/index.ts | 1 + .../saved_objects/migrations/to_v8_6_0.ts | 2 + .../services/epm/package_service.test.ts | 2 +- .../server/services/epm/package_service.ts | 13 +- .../epm/packages/bulk_install_packages.ts | 4 +- .../server/services/epm/packages/get.test.ts | 7 + .../fleet/server/services/epm/packages/get.ts | 19 ++- .../epm/packages/get_prerelease_setting.ts | 25 ++++ .../server/services/epm/packages/install.ts | 4 +- .../server/services/epm/registry/index.ts | 28 +++- .../fleet/server/services/package_policy.ts | 6 + .../plugins/fleet/server/services/settings.ts | 2 +- .../fleet/server/types/rest_spec/epm.ts | 20 ++- .../fleet/server/types/rest_spec/settings.ts | 1 + .../apis/epm/bulk_upgrade.ts | 8 +- .../apis/epm/custom_ingest_pipeline.ts | 2 +- .../fleet_api_integration/apis/epm/delete.ts | 1 + .../apis/epm/final_pipeline.ts | 2 +- .../fleet_api_integration/apis/epm/get.ts | 9 +- .../apis/epm/install_by_upload.ts | 1 + .../apis/epm/install_error_rollback.ts | 5 +- .../apis/epm/install_overrides.ts | 1 + .../apis/epm/install_prerelease.ts | 1 + .../epm/install_remove_kbn_assets_in_space.ts | 1 + .../apis/epm/install_remove_multiple.ts | 1 + .../apis/epm/install_tag_assets.ts | 1 + .../apis/epm/install_update.ts | 1 + .../fleet_api_integration/apis/epm/list.ts | 1 + .../apis/epm/package_install_complete.ts | 1 + .../fleet_api_integration/apis/epm/setup.ts | 1 + .../fleet_api_integration/apis/fleet_setup.ts | 2 +- .../apis/package_policy/delete.ts | 1 + x-pack/test/fleet_api_integration/helpers.ts | 16 ++ .../maps/group4/geofile_wizard_auto_open.ts | 2 +- .../services/uptime/synthetics_package.ts | 2 +- 66 files changed, 886 insertions(+), 148 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/hooks/use_categories.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/hooks/use_packages.tsx create mode 100644 x-pack/plugins/fleet/public/services/package_prerelease.test.ts create mode 100644 x-pack/plugins/fleet/public/services/package_prerelease.ts create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.ts diff --git a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts index b1aa1e5df9231..af572532a13e6 100644 --- a/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts +++ b/src/core/server/integration_tests/saved_objects/migrations/check_registered_types.test.ts @@ -99,7 +99,7 @@ describe('checking migration metadata changes on all registered SO types', () => "ingest-download-sources": "1e69dabd6db5e320fe08c5bda8f35f29bafc6b54", "ingest-outputs": "29b867bf7bfd28b1e17c84697dce5c6d078f9705", "ingest-package-policies": "e8707a8c7821ea085e67c2d213e24efa56307393", - "ingest_manager_settings": "bb71f20e36a9ac3a2e46d9345e2caa96e7bf8c22", + "ingest_manager_settings": "6f36714825cc15ea8d7cda06fde7851611a532b4", "inventory-view": "bc2bd1e7ec7c186159447ab228d269f22bd39056", "kql-telemetry": "29544cd7d3b767c5399878efae6bd724d24c03fd", "legacy-url-alias": "7172dfd54f2e0c89fe263fd7095519b2d826a930", diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index b2ccff5e7188b..ab8747170f760 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -247,7 +247,35 @@ } }, "operationId": "get-package-categories" - } + }, + "parameters": [ + { + "in": "query", + "name": "prerelease", + "schema": { + "type": "boolean", + "default": false + }, + "description": "Whether to include prerelease packages in categories count (e.g. beta, rc, preview) " + }, + { + "in": "query", + "name": "experimental", + "deprecated": true, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "include_policy_templates", + "schema": { + "type": "boolean", + "default": false + } + } + ] }, "/epm/packages/limited": { "get": { @@ -304,6 +332,31 @@ "default": false }, "description": "Whether to exclude the install status of each package. Enabling this option will opt in to caching for the response via `cache-control` headers. If you don't need up-to-date installation info for a package, and are querying for a list of available packages, providing this flag can improve performance substantially." + }, + { + "in": "query", + "name": "prerelease", + "schema": { + "type": "boolean", + "default": false + }, + "description": "Whether to return prerelease versions of packages (e.g. beta, rc, preview) " + }, + { + "in": "query", + "name": "experimental", + "deprecated": true, + "schema": { + "type": "boolean", + "default": false + } + }, + { + "in": "query", + "name": "category", + "schema": { + "type": "string" + } } ] }, @@ -4206,6 +4259,9 @@ "items": { "type": "string" } + }, + "prerelease_integrations_enabled": { + "type": "boolean" } }, "required": [ diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 139711f13b899..eca789024e4f3 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -154,6 +154,26 @@ paths: schema: $ref: '#/components/schemas/get_categories_response' operationId: get-package-categories + parameters: + - in: query + name: prerelease + schema: + type: boolean + default: false + description: >- + Whether to include prerelease packages in categories count (e.g. beta, + rc, preview) + - in: query + name: experimental + deprecated: true + schema: + type: boolean + default: false + - in: query + name: include_policy_templates + schema: + type: boolean + default: false /epm/packages/limited: get: summary: Packages - Get limited list @@ -196,6 +216,24 @@ paths: headers. If you don't need up-to-date installation info for a package, and are querying for a list of available packages, providing this flag can improve performance substantially. + - in: query + name: prerelease + schema: + type: boolean + default: false + description: >- + Whether to return prerelease versions of packages (e.g. beta, rc, + preview) + - in: query + name: experimental + deprecated: true + schema: + type: boolean + default: false + - in: query + name: category + schema: + type: string /epm/packages/_bulk: post: summary: Packages - Bulk install @@ -2617,6 +2655,8 @@ components: type: array items: type: string + prerelease_integrations_enabled: + type: boolean required: - fleet_server_hosts - id diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/settings.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/settings.yaml index 280460771989e..145b598267a0a 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/settings.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/settings.yaml @@ -9,6 +9,8 @@ properties: type: array items: type: string + prerelease_integrations_enabled: + type: boolean required: - fleet_server_hosts - id diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml index 1f2c3930d6ba3..9a69a930fa988 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@categories.yaml @@ -9,3 +9,22 @@ get: schema: $ref: ../components/schemas/get_categories_response.yaml operationId: get-package-categories +parameters: + - in: query + name: prerelease + schema: + type: boolean + default: false + description: >- + Whether to include prerelease packages in categories count (e.g. beta, rc, preview) + - in: query + name: experimental + deprecated: true + schema: + type: boolean + default: false + - in: query + name: include_policy_templates + schema: + type: boolean + default: false \ No newline at end of file diff --git a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml index 9c29b9d18357c..a6332360283bd 100644 --- a/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml +++ b/x-pack/plugins/fleet/common/openapi/paths/epm@packages.yaml @@ -20,3 +20,20 @@ parameters: caching for the response via `cache-control` headers. If you don't need up-to-date installation info for a package, and are querying for a list of available packages, providing this flag can improve performance substantially. + - in: query + name: prerelease + schema: + type: boolean + default: false + description: >- + Whether to return prerelease versions of packages (e.g. beta, rc, preview) + - in: query + name: experimental + deprecated: true + schema: + type: boolean + default: false + - in: query + name: category + schema: + type: string diff --git a/x-pack/plugins/fleet/common/types/models/settings.ts b/x-pack/plugins/fleet/common/types/models/settings.ts index 5a33fea910446..c70fa944e6c24 100644 --- a/x-pack/plugins/fleet/common/types/models/settings.ts +++ b/x-pack/plugins/fleet/common/types/models/settings.ts @@ -10,6 +10,7 @@ import type { SavedObjectAttributes } from '@kbn/core/public'; export interface BaseSettings { has_seen_add_data_notice?: boolean; fleet_server_hosts?: string[]; + prerelease_integrations_enabled: boolean; } export interface Settings extends BaseSettings { diff --git a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts index e12bdbb202321..105558e0d0620 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -17,7 +17,9 @@ import type { export interface GetCategoriesRequest { query: { + // deprecated in 8.6 experimental?: boolean; + prerelease?: boolean; include_policy_templates?: boolean; }; } @@ -31,7 +33,9 @@ export interface GetCategoriesResponse { export interface GetPackagesRequest { query: { category?: string; + // deprecated in 8.6 experimental?: boolean; + prerelease?: boolean; excludeInstallStatus?: boolean; }; } diff --git a/x-pack/plugins/fleet/cypress/e2e/install_assets.cy.ts b/x-pack/plugins/fleet/cypress/e2e/install_assets.cy.ts index 81ef56a4b1f52..4234df15d861e 100644 --- a/x-pack/plugins/fleet/cypress/e2e/install_assets.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/install_assets.cy.ts @@ -35,7 +35,7 @@ describe('Install unverified package assets', () => { }).as('installAssets'); // save mocking out the whole package response, but make it so that fleet server is always uninstalled - cy.intercept('GET', '/api/fleet/epm/packages/fleet_server', (req) => { + cy.intercept('GET', '/api/fleet/epm/packages/fleet_server*', (req) => { req.continue((res) => { if (res.body?.item?.savedObject) { delete res.body.item.savedObject; diff --git a/x-pack/plugins/fleet/cypress/e2e/integrations_mock.cy.ts b/x-pack/plugins/fleet/cypress/e2e/integrations_mock.cy.ts index ce207cd3598e2..3095f628599d6 100644 --- a/x-pack/plugins/fleet/cypress/e2e/integrations_mock.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/integrations_mock.cy.ts @@ -16,7 +16,7 @@ describe('Add Integration - Mock API', () => { const oldVersion = '0.3.3'; const newVersion = '1.3.4'; beforeEach(() => { - cy.intercept('/api/fleet/epm/packages?experimental=true', { + cy.intercept('/api/fleet/epm/packages?prerelease=true', { items: [ { name: 'apache', @@ -28,7 +28,7 @@ describe('Add Integration - Mock API', () => { ], }); - cy.intercept(`/api/fleet/epm/packages/apache/${oldVersion}`, { + cy.intercept(`/api/fleet/epm/packages/apache/${oldVersion}*`, { item: { name: 'apache', version: oldVersion, @@ -99,7 +99,7 @@ describe('Add Integration - Mock API', () => { cy.getBySel(INTEGRATION_POLICIES_UPGRADE_CHECKBOX).uncheck({ force: true }); - cy.intercept(`/api/fleet/epm/packages/apache/${newVersion}`, { + cy.intercept(`/api/fleet/epm/packages/apache/${newVersion}*`, { item: { name: 'apache', version: newVersion, diff --git a/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts b/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts index c3bee2d758df0..3b7c29561bc93 100644 --- a/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts +++ b/x-pack/plugins/fleet/cypress/e2e/integrations_real.cy.ts @@ -174,7 +174,7 @@ describe('Add Integration - Real API', () => { setupIntegrations(); cy.getBySel(getIntegrationCategories('aws')).click(); cy.getBySel(INTEGRATIONS_SEARCHBAR.BADGE).contains('AWS').should('exist'); - cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length', 30); + cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length', 28); cy.getBySel(INTEGRATIONS_SEARCHBAR.INPUT).clear().type('Cloud'); cy.getBySel(INTEGRATION_LIST).find('.euiCard').should('have.length', 3); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/debug/components/integration_debugger.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/debug/components/integration_debugger.tsx index 9c3fa21c752f8..30fc1b84964f3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/debug/components/integration_debugger.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/debug/components/integration_debugger.tsx @@ -39,7 +39,7 @@ import { queryClient } from '..'; import { pkgKeyFromPackageInfo } from '../../../services'; const fetchInstalledIntegrations = async () => { - const response = await sendGetPackages({ experimental: true }); + const response = await sendGetPackages({ prerelease: true }); if (response.error) { throw new Error(response.error.message); diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts index 5b6b19af169f0..76b6b49c8c5cb 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts @@ -14,3 +14,5 @@ export * from './use_agent_policy_context'; export * from './use_integrations_state'; export * from './use_confirm_force_install'; export * from './use_confirm_open_unverified'; +export * from './use_packages'; +export * from './use_categories'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_categories.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_categories.tsx new file mode 100644 index 0000000000000..8b441a229e9b7 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_categories.tsx @@ -0,0 +1,55 @@ +/* + * 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 { useEffect, useCallback, useState } from 'react'; + +import type { RequestError } from '../../fleet/hooks'; +import { sendGetCategories } from '../../fleet/hooks'; +import type { GetCategoriesResponse } from '../types'; + +export function useCategories(prerelease?: boolean) { + const [data, setData] = useState(); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [isPrereleaseEnabled, setIsPrereleaseEnabled] = useState(prerelease); + + const fetchData = useCallback(async () => { + if (prerelease === undefined) { + return; + } + if (isPrereleaseEnabled === prerelease) { + return; + } + setIsPrereleaseEnabled(prerelease); + setIsLoading(true); + try { + const res = await sendGetCategories({ + include_policy_templates: true, + prerelease, + }); + if (res.error) { + throw res.error; + } + if (res.data) { + setData(res.data); + } + } catch (err) { + setError(err); + } + setIsLoading(false); + }, [prerelease, isPrereleaseEnabled]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + data, + error, + isLoading, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_packages.tsx new file mode 100644 index 0000000000000..efb2f96ed57f3 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_packages.tsx @@ -0,0 +1,56 @@ +/* + * 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 { useEffect, useCallback, useState } from 'react'; + +import type { RequestError } from '../../fleet/hooks'; +import { sendGetPackages } from '../../fleet/hooks'; +import type { GetPackagesResponse } from '../types'; + +export function usePackages(prerelease?: boolean) { + const [data, setData] = useState(); + const [error, setError] = useState(); + const [isLoading, setIsLoading] = useState(false); + const [isPrereleaseEnabled, setIsPrereleaseEnabled] = useState(prerelease); + + const fetchData = useCallback(async () => { + if (prerelease === undefined) { + return; + } + if (isPrereleaseEnabled === prerelease) { + return; + } + setIsPrereleaseEnabled(prerelease); + setIsLoading(true); + try { + const res = await sendGetPackages({ + category: '', + excludeInstallStatus: true, + prerelease, + }); + if (res.error) { + throw res.error; + } + if (res.data) { + setData(res.data); + } + } catch (err) { + setError(err); + } + setIsLoading(false); + }, [prerelease, isPrereleaseEnabled]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + return { + data, + error, + isLoading, + }; +} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.stories.tsx index 86b34f2415e2e..ebc18d84487db 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.stories.tsx @@ -32,5 +32,11 @@ export default { } as Meta; export const IntegrationPreference = () => { - return ; + return ( + {}} + /> + ); }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx index b99683adbf8f4..4e4aafa271b3b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/integration_preference.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; @@ -20,9 +20,10 @@ import { EuiIconTip, EuiFlexGroup, EuiFlexItem, + EuiSwitch, } from '@elastic/eui'; -import { useStartServices } from '../../../hooks'; +import { sendPutSettings, useGetSettings, useStartServices } from '../../../hooks'; export type IntegrationPreferenceType = 'recommended' | 'beats' | 'agent'; @@ -34,6 +35,7 @@ interface Option { export interface Props { initialType: IntegrationPreferenceType; onChange: (type: IntegrationPreferenceType) => void; + onPrereleaseEnabledChange: (prerelease: boolean) => void; } const recommendedTooltip = ( @@ -47,6 +49,10 @@ const Item = styled(EuiFlexItem)` padding-left: ${(props) => props.theme.eui.euiSizeXS}; `; +const EuiSwitchNoWrap = styled(EuiSwitch)` + white-space: nowrap; +`; + const options: Option[] = [ { type: 'recommended', @@ -77,11 +83,46 @@ const options: Option[] = [ }, ]; -export const IntegrationPreference = ({ initialType, onChange }: Props) => { +export const IntegrationPreference = ({ + initialType, + onChange, + onPrereleaseEnabledChange, +}: Props) => { const [idSelected, setIdSelected] = React.useState(initialType); const { docLinks } = useStartServices(); + const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState< + boolean | undefined + >(undefined); + + const { data: settings, error: settingsError } = useGetSettings(); + + useEffect(() => { + const isEnabled = Boolean(settings?.item.prerelease_integrations_enabled); + if (settings?.item) { + setPrereleaseIntegrationsEnabled(isEnabled); + } else if (settingsError) { + setPrereleaseIntegrationsEnabled(false); + } + }, [settings?.item, settingsError]); + + useEffect(() => { + if (prereleaseIntegrationsEnabled !== undefined) { + onPrereleaseEnabledChange(prereleaseIntegrationsEnabled); + } + }, [onPrereleaseEnabledChange, prereleaseIntegrationsEnabled]); + + const updateSettings = useCallback(async (prerelease: boolean) => { + const res = await sendPutSettings({ + prerelease_integrations_enabled: prerelease, + }); + + if (res.error) { + throw res.error; + } + }, []); + const link = ( { label: option.label, })); + const onPrereleaseSwitchChange = ( + event: React.BaseSyntheticEvent< + React.MouseEvent, + HTMLButtonElement, + EventTarget & { checked: boolean } + > + ) => { + const isChecked = event.target.checked; + setPrereleaseIntegrationsEnabled(isChecked); + updateSettings(isChecked); + }; + return ( + {prereleaseIntegrationsEnabled !== undefined && ( + + )} + {title} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx index 49a8cbeb37d21..cb03f5321e578 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.test.tsx @@ -17,6 +17,7 @@ import type { GetInfoResponse, GetPackagePoliciesResponse, GetStatsResponse, + GetSettingsResponse, } from '../../../../../../../common/types/rest_spec'; import type { DetailViewPanelName, @@ -79,11 +80,23 @@ describe('when on integration detail', () => { }); }); - describe('and the package is not installed', () => { + function mockGAAndPrereleaseVersions(pkgVersion: string) { + const unInstalledPackage = mockedApi.responseProvider.epmGetInfo('nginx'); + unInstalledPackage.item.status = 'not_installed'; + unInstalledPackage.item.version = pkgVersion; + mockedApi.responseProvider.epmGetInfo.mockImplementation((name, version, query) => { + if (query?.prerelease === false) { + const gaPackage = { item: { ...unInstalledPackage.item } }; + gaPackage.item.version = '1.0.0'; + return gaPackage; + } + return unInstalledPackage; + }); + } + + describe('and the package is not installed and prerelease enabled', () => { beforeEach(async () => { - const unInstalledPackage = mockedApi.responseProvider.epmGetInfo(); - unInstalledPackage.item.status = 'not_installed'; - mockedApi.responseProvider.epmGetInfo.mockReturnValue(unInstalledPackage); + mockGAAndPrereleaseVersions('1.0.0-beta'); await render(); }); @@ -96,6 +109,39 @@ describe('when on integration detail', () => { await mockedApi.waitForApi(); expect(renderResult.queryByTestId('tab-policies')).toBeNull(); }); + + it('should display version select if prerelease setting enabled and prererelase version available', async () => { + await mockedApi.waitForApi(); + const versionSelect = renderResult.queryByTestId('versionSelect'); + expect(versionSelect?.textContent).toEqual('1.0.0-beta1.0.0'); + expect((versionSelect as any)?.value).toEqual('1.0.0-beta'); + }); + + it('should display prerelease callout if prerelease setting enabled and prerelease version available', async () => { + await mockedApi.waitForApi(); + const calloutTitle = renderResult.getByTestId('prereleaseCallout'); + expect(calloutTitle).toBeInTheDocument(); + const calloutGABtn = renderResult.getByTestId('switchToGABtn'); + expect((calloutGABtn as any)?.href).toEqual( + 'http://localhost/mock/app/integrations/detail/nginx-1.0.0/overview' + ); + }); + }); + + describe('and the package is not installed and prerelease disabled', () => { + beforeEach(async () => { + mockGAAndPrereleaseVersions('1.0.0'); + mockedApi.responseProvider.getSettings.mockReturnValue({ + item: { prerelease_integrations_enabled: false, id: '', fleet_server_hosts: [] }, + }); + await render(); + }); + + it('should display version text and no callout if prerelease setting disabled', async () => { + await mockedApi.waitForApi(); + expect((renderResult.queryByTestId('versionText') as any)?.textContent).toEqual('1.0.0'); + expect(renderResult.queryByTestId('prereleaseCallout')).toBeNull(); + }); }); describe('and a custom UI extension is NOT registered', () => { @@ -267,13 +313,16 @@ interface MockedApi< } interface EpmPackageDetailsResponseProvidersMock { - epmGetInfo: jest.MockedFunction<() => GetInfoResponse>; + epmGetInfo: jest.MockedFunction< + (pkgName: string, pkgVersion?: string, options?: { prerelease?: boolean }) => GetInfoResponse + >; epmGetFile: jest.MockedFunction<() => string>; epmGetStats: jest.MockedFunction<() => GetStatsResponse>; fleetSetup: jest.MockedFunction<() => GetFleetStatusResponse>; packagePolicyList: jest.MockedFunction<() => GetPackagePoliciesResponse>; agentPolicyList: jest.MockedFunction<() => GetAgentPoliciesResponse>; appCheckPermissions: jest.MockedFunction<() => CheckPermissionsResponse>; + getSettings: jest.MockedFunction<() => GetSettingsResponse>; } const mockApiCalls = ( @@ -753,6 +802,8 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos success: true, }; + const getSettingsResponse = { item: { prerelease_integrations_enabled: true } }; + const mockedApiInterface: MockedApi = { waitForApi() { return new Promise((resolve) => { @@ -771,14 +822,19 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos packagePolicyList: jest.fn().mockReturnValue(packagePoliciesResponse), agentPolicyList: jest.fn().mockReturnValue(agentPoliciesResponse), appCheckPermissions: jest.fn().mockReturnValue(appCheckPermissionsResponse), + getSettings: jest.fn().mockReturnValue(getSettingsResponse), }, }; - http.get.mockImplementation(async (path: any) => { + http.get.mockImplementation((async (path: any, options: any) => { if (typeof path === 'string') { if (path === epmRouteService.getInfoPath(`nginx`, `0.3.7`)) { markApiCallAsHandled(); - return mockedApiInterface.responseProvider.epmGetInfo(); + return mockedApiInterface.responseProvider.epmGetInfo('nginx'); + } + if (path === epmRouteService.getInfoPath(`nginx`)) { + markApiCallAsHandled(); + return mockedApiInterface.responseProvider.epmGetInfo('nginx', undefined, options.query); } if (path === epmRouteService.getFilePath('/package/nginx/0.3.7/docs/README.md')) { @@ -820,13 +876,16 @@ On Windows, the module was tested with Nginx installed from the Chocolatey repos if (path === '/api/fleet/agents') { return Promise.resolve(); } + if (path === '/api/fleet/settings') { + return mockedApiInterface.responseProvider.getSettings(); + } const err = new Error(`API [GET ${path}] is not MOCKED!`); // eslint-disable-next-line no-console console.error(err); throw err; } - }); + }) as any); return mockedApiInterface; }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index d649bede3db44..42c5cc535d6b7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -17,6 +17,7 @@ import { EuiDescriptionListTitle, EuiFlexGroup, EuiFlexItem, + EuiSelect, EuiSpacer, EuiText, } from '@elastic/eui'; @@ -36,9 +37,10 @@ import { useAuthz, usePermissionCheck, useIntegrationsStateContext, + useGetSettings, } from '../../../../hooks'; import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants'; -import { ExperimentalFeaturesService } from '../../../../services'; +import { ExperimentalFeaturesService, isPackagePrerelease } from '../../../../services'; import { useGetPackageInfoByKey, useLink, @@ -107,7 +109,7 @@ export function Detail() { const { getId: getAgentPolicyId } = useAgentPolicyContext(); const { getFromIntegrations } = useIntegrationsStateContext(); const { pkgkey, panel } = useParams(); - const { getHref } = useLink(); + const { getHref, getPath } = useLink(); const canInstallPackages = useAuthz().integrations.installPackages; const canReadPackageSettings = useAuthz().integrations.readPackageSettings; const canReadIntegrationPolicies = useAuthz().integrations.readIntegrationPolicies; @@ -153,6 +155,17 @@ export function Detail() { packageInfo.savedObject && semverLt(packageInfo.savedObject.attributes.version, packageInfo.latestVersion); + const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState< + boolean | undefined + >(); + + const { data: settings } = useGetSettings(); + + useEffect(() => { + const isEnabled = Boolean(settings?.item.prerelease_integrations_enabled); + setPrereleaseIntegrationsEnabled(isEnabled); + }, [settings?.item.prerelease_integrations_enabled]); + const { pkgName, pkgVersion } = splitPkgKey(pkgkey); // Fetch package info const { @@ -161,7 +174,34 @@ export function Detail() { isLoading: packageInfoLoading, isInitialRequest: packageIsInitialRequest, resendRequest: refreshPackageInfo, - } = useGetPackageInfoByKey(pkgName, pkgVersion); + } = useGetPackageInfoByKey(pkgName, pkgVersion, { + prerelease: prereleaseIntegrationsEnabled, + }); + + const [latestGAVersion, setLatestGAVersion] = useState(); + const [latestPrereleaseVersion, setLatestPrereleaseVersion] = useState(); + + // fetch latest GA version (prerelease=false) + const { data: packageInfoLatestGAData } = useGetPackageInfoByKey(pkgName, '', { + prerelease: false, + }); + + useEffect(() => { + const pkg = packageInfoLatestGAData?.item; + const isGAVersion = pkg && !isPackagePrerelease(pkg.version); + if (isGAVersion) { + setLatestGAVersion(pkg.version); + } + }, [packageInfoLatestGAData?.item]); + + // fetch latest Prerelease version (prerelease=true) + const { data: packageInfoLatestPrereleaseData } = useGetPackageInfoByKey(pkgName, '', { + prerelease: true, + }); + + useEffect(() => { + setLatestPrereleaseVersion(packageInfoLatestPrereleaseData?.item.version); + }, [packageInfoLatestPrereleaseData?.item.version]); const { isFirstTimeAgentUser = false, isLoading: firstTimeUserLoading } = useIsFirstTimeAgentUser(); @@ -326,6 +366,46 @@ export function Detail() { ] ); + const showVersionSelect = useMemo( + () => + prereleaseIntegrationsEnabled && + latestGAVersion && + latestPrereleaseVersion && + latestGAVersion !== latestPrereleaseVersion && + (!packageInfo?.version || + packageInfo.version === latestGAVersion || + packageInfo.version === latestPrereleaseVersion), + [prereleaseIntegrationsEnabled, latestGAVersion, latestPrereleaseVersion, packageInfo?.version] + ); + + const versionOptions = useMemo( + () => [ + { + value: latestPrereleaseVersion, + text: latestPrereleaseVersion, + }, + { + value: latestGAVersion, + text: latestGAVersion, + }, + ], + [latestPrereleaseVersion, latestGAVersion] + ); + + const versionLabel = i18n.translate('xpack.fleet.epm.versionLabel', { + defaultMessage: 'Version', + }); + + const onVersionChange = useCallback( + (version: string, packageName: string) => { + const path = getPath('integration_details_overview', { + pkgkey: `${packageName}-${version}`, + }); + history.push(path); + }, + [getPath, history] + ); + const headerRightContent = useMemo( () => packageInfo ? ( @@ -334,12 +414,24 @@ export function Detail() { {[ { - label: i18n.translate('xpack.fleet.epm.versionLabel', { - defaultMessage: 'Version', - }), + label: showVersionSelect ? undefined : versionLabel, content: ( - {packageInfo.version} + + {showVersionSelect ? ( + + onVersionChange(event.target.value, packageInfo.name) + } + /> + ) : ( +
{packageInfo.version}
+ )} +
{updateAvailable ? ( @@ -416,6 +508,10 @@ export function Detail() { missingSecurityConfiguration, integrationInfo?.title, handleAddIntegrationPolicyClick, + onVersionChange, + showVersionSelect, + versionLabel, + versionOptions, ] ); @@ -605,7 +701,11 @@ export function Detail() { ) : ( - + diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx index 51991fb8aab33..21b3fd0f4f11c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/overview/overview.tsx @@ -6,14 +6,14 @@ */ import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; -import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink } from '@elastic/eui'; +import { EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiLink, EuiButton } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { isIntegrationPolicyTemplate } from '../../../../../../../../common/services'; -import { useFleetStatus, useStartServices } from '../../../../../../../hooks'; -import { isPackageUnverified } from '../../../../../../../services'; +import { useFleetStatus, useLink, useStartServices } from '../../../../../../../hooks'; +import { isPackagePrerelease, isPackageUnverified } from '../../../../../../../services'; import type { PackageInfo, RegistryPolicyTemplate } from '../../../../../types'; import { Screenshots } from './screenshots'; @@ -23,6 +23,7 @@ import { Details } from './details'; interface Props { packageInfo: PackageInfo; integrationInfo?: RegistryPolicyTemplate; + latestGAVersion?: string; } const LeftColumn = styled(EuiFlexItem)` @@ -66,48 +67,97 @@ const UnverifiedCallout: React.FC = () => { ); }; -export const OverviewPage: React.FC = memo(({ packageInfo, integrationInfo }) => { - const screenshots = useMemo( - () => integrationInfo?.screenshots || packageInfo.screenshots || [], - [integrationInfo, packageInfo.screenshots] - ); - const { packageVerificationKeyId } = useFleetStatus(); - const isUnverified = isPackageUnverified(packageInfo, packageVerificationKeyId); +const PrereleaseCallout: React.FC<{ + packageName: string; + latestGAVersion?: string; + packageTitle: string; +}> = ({ packageName, packageTitle, latestGAVersion }) => { + const { getHref } = useLink(); + const overviewPathLatestGA = getHref('integration_details_overview', { + pkgkey: `${packageName}-${latestGAVersion}`, + }); + return ( - - - - {isUnverified && } - {packageInfo.readme ? ( - - ) : null} - - - - {screenshots.length ? ( - - + + {latestGAVersion && ( +

+ + - - ) : null} - -

- - - - + +

+ )} + + + ); -}); +}; + +export const OverviewPage: React.FC = memo( + ({ packageInfo, integrationInfo, latestGAVersion }) => { + const screenshots = useMemo( + () => integrationInfo?.screenshots || packageInfo.screenshots || [], + [integrationInfo, packageInfo.screenshots] + ); + const { packageVerificationKeyId } = useFleetStatus(); + const isUnverified = isPackageUnverified(packageInfo, packageVerificationKeyId); + const isPrerelease = isPackagePrerelease(packageInfo.version); + return ( + + + + {isUnverified && } + {isPrerelease && ( + + )} + {packageInfo.readme ? ( + + ) : null} + + + + {screenshots.length ? ( + + + + ) : null} + +
+ + + + + ); + } +); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index 7f56592cdc84b..a763d26e2821b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -20,19 +20,17 @@ import { isIntegrationPolicyTemplate, } from '../../../../../../../common/services'; -import { useStartServices } from '../../../../hooks'; +import { useCategories, usePackages, useStartServices } from '../../../../hooks'; import { pagePathGetters } from '../../../../constants'; import { - useGetCategories, - useGetPackages, useBreadcrumbs, useGetAppendCustomIntegrations, useGetReplacementCustomIntegrations, useLink, } from '../../../../hooks'; import { doesPackageHaveIntegrations } from '../../../../services'; -import type { GetPackagesResponse, PackageList } from '../../../../types'; +import type { PackageList } from '../../../../types'; import { PackageListGrid } from '../../components/package_list_grid'; import type { PackageListItem } from '../../../../types'; @@ -183,11 +181,11 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => { // TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http` // or `location` to load data. Ideally, we'll split this into "connected" and "pure" components. -export const AvailablePackages: React.FC<{ - allPackages?: GetPackagesResponse | null; - isLoading: boolean; -}> = ({ allPackages, isLoading }) => { +export const AvailablePackages: React.FC<{}> = ({}) => { const [preference, setPreference] = useState('recommended'); + const [prereleaseIntegrationsEnabled, setPrereleaseIntegrationsEnabled] = React.useState< + boolean | undefined + >(undefined); useBreadcrumbs('integrations_all'); @@ -222,10 +220,7 @@ export const AvailablePackages: React.FC<{ data: eprPackages, isLoading: isLoadingAllPackages, error: eprPackageLoadingError, - } = useGetPackages({ - category: '', - excludeInstallStatus: true, - }); + } = usePackages(prereleaseIntegrationsEnabled); // Remove Kubernetes package granularity if (eprPackages?.items) { @@ -276,9 +271,7 @@ export const AvailablePackages: React.FC<{ data: eprCategories, isLoading: isLoadingCategories, error: eprCategoryLoadingError, - } = useGetCategories({ - include_policy_templates: true, - }); + } = useCategories(prereleaseIntegrationsEnabled); const categories: CategoryFacet[] = useMemo(() => { const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories @@ -306,7 +299,13 @@ export const AvailablePackages: React.FC<{ let controls = [ - + { + setPrereleaseIntegrationsEnabled(isEnabled); + }} + /> , ]; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 18d96fbd66346..5b9227553c79e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -15,7 +15,7 @@ import { installationStatuses } from '../../../../../../../common/constants'; import type { DynamicPage, DynamicPagePathValues, StaticPage } from '../../../../constants'; import { INTEGRATIONS_ROUTING_PATHS, INTEGRATIONS_SEARCH_QUERYPARAM } from '../../../../constants'; import { DefaultLayout } from '../../../../layouts'; -import { isPackageUnverified } from '../../../../services'; +import { isPackagePrerelease, isPackageUnverified } from '../../../../services'; import type { PackageListItem } from '../../../../types'; @@ -65,19 +65,24 @@ export const mapToCard = ({ let isUnverified = false; + let version = 'version' in item ? item.version || '' : ''; + if (item.type === 'ui_link') { uiInternalPathUrl = item.id.includes('language_client.') ? addBasePath(item.uiInternalPath) : item.uiExternalLink || getAbsolutePath(item.uiInternalPath); } else { - let urlVersion = item.version; - if ('savedObject' in item) { - urlVersion = item.savedObject.attributes.version || item.version; + // installed package + if ( + ['updates_available', 'installed'].includes(selectedCategory ?? '') && + 'savedObject' in item + ) { + version = item.savedObject.attributes.version || item.version; isUnverified = isPackageUnverified(item, packageVerificationKeyId); } const url = getHref('integration_details_overview', { - pkgkey: `${item.name}-${urlVersion}`, + pkgkey: `${item.name}-${version}`, ...(item.integration ? { integration: item.integration } : {}), }); @@ -90,6 +95,9 @@ export const mapToCard = ({ } else if ((item as CustomIntegration).isBeta === true) { release = 'beta'; } + if (!isPackagePrerelease(version)) { + release = 'ga'; + } return { id: `${item.type === 'ui_link' ? 'ui_link' : 'epr'}:${item.id}`, @@ -100,7 +108,7 @@ export const mapToCard = ({ fromIntegrations: selectedCategory, integration: 'integration' in item ? item.integration || '' : '', name: 'name' in item ? item.name : item.id, - version: 'version' in item ? item.version || '' : '', + version, release, categories: ((item.categories || []) as string[]).filter((c: string) => !!c), isUnverified, @@ -108,8 +116,9 @@ export const mapToCard = ({ }; export const EPMHomePage: React.FC = () => { + // loading packages to find installed ones const { data: allPackages, isLoading } = useGetPackages({ - experimental: true, + prerelease: true, }); const installedPackages = useMemo( @@ -132,7 +141,7 @@ export const EPMHomePage: React.FC = () => { - + diff --git a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx index 5a0b6285c71db..e4dabcff4e8a0 100644 --- a/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_package_installations.tsx @@ -28,7 +28,7 @@ interface UpdatableIntegration { export const usePackageInstallations = () => { const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({ - experimental: true, + prerelease: true, }); const { data: agentPolicyData, isLoading: isLoadingPolicies } = useGetAgentPolicies({ diff --git a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts index 3a0033435ed9d..9c88cfae46c4d 100644 --- a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts +++ b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts @@ -44,7 +44,15 @@ export const useGetCategories = (query: GetCategoriesRequest['query'] = {}) => { return useRequest({ path: epmRouteService.getCategoriesPath(), method: 'get', - query: { experimental: true, ...query }, + query, + }); +}; + +export const sendGetCategories = (query: GetCategoriesRequest['query'] = {}) => { + return sendRequest({ + path: epmRouteService.getCategoriesPath(), + method: 'get', + query, }); }; @@ -52,7 +60,7 @@ export const useGetPackages = (query: GetPackagesRequest['query'] = {}) => { return useRequest({ path: epmRouteService.getListPath(), method: 'get', - query: { experimental: true, ...query }, + query, }); }; @@ -60,7 +68,7 @@ export const sendGetPackages = (query: GetPackagesRequest['query'] = {}) => { return sendRequest({ path: epmRouteService.getListPath(), method: 'get', - query: { experimental: true, ...query }, + query, }); }; @@ -74,14 +82,22 @@ export const useGetLimitedPackages = () => { export const useGetPackageInfoByKey = ( pkgName: string, pkgVersion?: string, - ignoreUnverified: boolean = false + options?: { + ignoreUnverified?: boolean; + prerelease?: boolean; + } ) => { const confirmOpenUnverified = useConfirmOpenUnverified(); - const [ignoreUnverifiedQueryParam, setIgnoreUnverifiedQueryParam] = useState(ignoreUnverified); + const [ignoreUnverifiedQueryParam, setIgnoreUnverifiedQueryParam] = useState( + options?.ignoreUnverified + ); const res = useRequest({ path: epmRouteService.getInfoPath(pkgName, pkgVersion), method: 'get', - query: ignoreUnverifiedQueryParam ? { ignoreUnverified: ignoreUnverifiedQueryParam } : {}, + query: { + ...options, + ...(ignoreUnverifiedQueryParam ? { ignoreUnverified: ignoreUnverifiedQueryParam } : {}), + }, }); useEffect(() => { @@ -111,12 +127,15 @@ export const useGetPackageStats = (pkgName: string) => { export const sendGetPackageInfoByKey = ( pkgName: string, pkgVersion?: string, - ignoreUnverified?: boolean + options?: { + ignoreUnverified?: boolean; + prerelease?: boolean; + } ) => { return sendRequest({ path: epmRouteService.getInfoPath(pkgName, pkgVersion), method: 'get', - query: ignoreUnverified ? { ignoreUnverified } : {}, + query: options, }); }; diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index d6167b4548e65..9d6ef9b4563ae 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -47,3 +47,4 @@ export { pkgKeyFromPackageInfo } from './pkg_key_from_package_info'; export { createExtensionRegistrationCallback } from './ui_extensions'; export { incrementPolicyName } from './increment_policy_name'; export { policyHasFleetServer } from './has_fleet_server'; +export { isPackagePrerelease } from './package_prerelease'; diff --git a/x-pack/plugins/fleet/public/services/package_prerelease.test.ts b/x-pack/plugins/fleet/public/services/package_prerelease.test.ts new file mode 100644 index 0000000000000..c8843d304de55 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/package_prerelease.test.ts @@ -0,0 +1,34 @@ +/* + * 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 { isPackagePrerelease } from './package_prerelease'; + +describe('isPackagePrerelease', () => { + it('should return prerelease true for 0.1.0', () => { + expect(isPackagePrerelease('0.1.0')).toBe(true); + }); + + it('should return prerelease false for 1.1.0', () => { + expect(isPackagePrerelease('1.1.0')).toBe(false); + }); + + it('should return prerelease true for 1.0.0-preview', () => { + expect(isPackagePrerelease('1.0.0-preview')).toBe(true); + }); + + it('should return prerelease true for 1.0.0-beta', () => { + expect(isPackagePrerelease('1.0.0-beta')).toBe(true); + }); + + it('should return prerelease true for 1.0.0-rc', () => { + expect(isPackagePrerelease('1.0.0-rc')).toBe(true); + }); + + it('should return prerelease true for 1.0.0-dev.0', () => { + expect(isPackagePrerelease('1.0.0-dev.0')).toBe(true); + }); +}); diff --git a/x-pack/plugins/fleet/public/services/package_prerelease.ts b/x-pack/plugins/fleet/public/services/package_prerelease.ts new file mode 100644 index 0000000000000..0df6e0deee409 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/package_prerelease.ts @@ -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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export function isPackagePrerelease(version: string): boolean { + // derive from semver + return version.startsWith('0') || version.includes('-'); +} diff --git a/x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.ts b/x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.ts index e07ff9f5a1808..4215a460a29cb 100644 --- a/x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.ts +++ b/x-pack/plugins/fleet/scripts/install_all_packages/install_all_packages.ts @@ -57,7 +57,7 @@ async function deletePackage(name: string, version: string) { async function getAllPackages() { const res = await fetch( - `${REGISTRY_URL}/search?experimental=true&kibana.version=${KIBANA_VERSION}`, + `${REGISTRY_URL}/search?prerelease=true&kibana.version=${KIBANA_VERSION}`, { headers: { accept: '*/*', diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index 5d177641daee5..ee69ae9fc8e65 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -572,7 +572,7 @@ export class FleetPlugin internalSoClient, this.getLogger() ); - return this.packageService; + return this.packageService!; } private getLogger(): Logger { diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 242e272cd184b..5ef609fe9b6cc 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -37,6 +37,7 @@ import type { GetStatsRequestSchema, FleetRequestHandler, UpdatePackageRequestSchema, + GetLimitedPackagesRequestSchema, } from '../../types'; import { bulkInstallPackages, @@ -67,7 +68,9 @@ export const getCategoriesHandler: FleetRequestHandler< TypeOf > = async (context, request, response) => { try { - const res = await getCategories(request.query); + const res = await getCategories({ + ...request.query, + }); const body: GetCategoriesResponse = { items: res, response: res, @@ -103,10 +106,17 @@ export const getListHandler: FleetRequestHandler< } }; -export const getLimitedListHandler: FleetRequestHandler = async (context, request, response) => { +export const getLimitedListHandler: FleetRequestHandler< + undefined, + TypeOf, + undefined +> = async (context, request, response) => { try { const savedObjectsClient = (await context.fleet).epm.internalSoClient; - const res = await getLimitedPackages({ savedObjectsClient }); + const res = await getLimitedPackages({ + savedObjectsClient, + prerelease: request.query.prerelease, + }); const body: GetLimitedPackagesResponse = { items: res, response: res, @@ -200,7 +210,7 @@ export const getInfoHandler: FleetRequestHandler< try { const savedObjectsClient = (await context.fleet).epm.internalSoClient; const { pkgName, pkgVersion } = request.params; - const { ignoreUnverified = false } = request.query; + const { ignoreUnverified = false, prerelease } = request.query; if (pkgVersion && !semverValid(pkgVersion)) { throw new FleetError('Package version is not a valid semver'); } @@ -210,6 +220,7 @@ export const getInfoHandler: FleetRequestHandler< pkgVersion: pkgVersion || '', skipArchive: true, ignoreUnverified, + prerelease, }); const body: GetInfoResponse = { item: res, @@ -307,7 +318,7 @@ const bulkInstallServiceResponseToHttpEntry = ( export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler< undefined, - undefined, + TypeOf, TypeOf > = async (context, request, response) => { const coreContext = await context.core; @@ -320,6 +331,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler< esClient, packagesToInstall: request.body.packages, spaceId, + prerelease: request.query.prerelease, }); const payload = bulkInstalledResponses.map(bulkInstallServiceResponseToHttpEntry); const body: BulkInstallPackagesResponse = { diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index d96d574f77bed..ee9caa4def673 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -204,6 +204,7 @@ export const createPackagePolicyHandler: FleetRequestHandler< pkgName: pkg.name, pkgVersion: pkg.version, ignoreUnverified: force, + prerelease: true, }); newPackagePolicy = simplifiedPackagePolicytoNewPackagePolicy(newPolicy, pkgInfo, { experimental_data_stream_features: pkg.experimental_data_stream_features, diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts index 4a943789cac68..5721d2e6b7ed6 100644 --- a/x-pack/plugins/fleet/server/saved_objects/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/index.ts @@ -70,6 +70,7 @@ const getSavedObjectTypes = ( properties: { fleet_server_hosts: { type: 'keyword' }, has_seen_add_data_notice: { type: 'boolean', index: false }, + prerelease_integrations_enabled: { type: 'boolean' }, }, }, migrations: { diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_6_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_6_0.ts index 5134249ddd1ef..e43406d859600 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_6_0.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/to_v8_6_0.ts @@ -16,5 +16,7 @@ export const migrateSettingsToV860: SavedObjectMigrationFn = // @ts-expect-error has_seen_fleet_migration_notice property does not exists anymore delete settingsDoc.attributes.has_seen_fleet_migration_notice; + settingsDoc.attributes.prerelease_integrations_enabled = false; + return settingsDoc; }; diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts index f4f853d778923..44d35f3e4c33c 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts @@ -92,7 +92,7 @@ function getTest( method: mocks.packageClient.fetchFindLatestPackage.bind(mocks.packageClient), args: ['package name'], spy: jest.spyOn(epmRegistry, 'fetchFindLatestPackageOrThrow'), - spyArgs: ['package name'], + spyArgs: ['package name', undefined], spyResponse: { name: 'fetchFindLatestPackage test' }, expectedReturnValue: { name: 'fetchFindLatestPackage test' }, }; diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.ts b/x-pack/plugins/fleet/server/services/epm/package_service.ts index 45fb673771327..f3d82f13d96ee 100644 --- a/x-pack/plugins/fleet/server/services/epm/package_service.ts +++ b/x-pack/plugins/fleet/server/services/epm/package_service.ts @@ -26,6 +26,7 @@ import { checkSuperuser } from '../../routes/security'; import { FleetUnauthorizedError } from '../../errors'; import { installTransforms, isTransform } from './elasticsearch/transform/install'; +import type { FetchFindLatestPackageOptions } from './registry'; import { fetchFindLatestPackageOrThrow, getPackage } from './registry'; import { ensureInstalledPackage, getInstallation } from './packages'; @@ -45,7 +46,10 @@ export interface PackageClient { spaceId?: string; }): Promise; - fetchFindLatestPackage(packageName: string): Promise; + fetchFindLatestPackage( + packageName: string, + options?: FetchFindLatestPackageOptions + ): Promise; getPackage( packageName: string, @@ -116,9 +120,12 @@ class PackageClientImpl implements PackageClient { }); } - public async fetchFindLatestPackage(packageName: string) { + public async fetchFindLatestPackage( + packageName: string, + options?: FetchFindLatestPackageOptions + ): Promise { await this.#runPreflight(); - return fetchFindLatestPackageOrThrow(packageName); + return fetchFindLatestPackageOrThrow(packageName, options); } public async getPackage( diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts index a9d027ce51c8a..66b9323dd0939 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts @@ -22,6 +22,7 @@ interface BulkInstallPackagesParams { force?: boolean; spaceId: string; preferredSource?: 'registry' | 'bundled'; + prerelease?: boolean; } export async function bulkInstallPackages({ @@ -30,6 +31,7 @@ export async function bulkInstallPackages({ esClient, spaceId, force, + prerelease, }: BulkInstallPackagesParams): Promise { const logger = appContextService.getLogger(); @@ -39,7 +41,7 @@ export async function bulkInstallPackages({ return Promise.resolve(pkg); } - return Registry.fetchFindLatestPackageOrThrow(pkg); + return Registry.fetchFindLatestPackageOrThrow(pkg, { prerelease }); }) ); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts index 20ed655d97176..19ced885822a4 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.test.ts @@ -22,10 +22,17 @@ import { appContextService } from '../../app_context'; import { PackageNotFoundError } from '../../../errors'; +import { getSettings } from '../../settings'; + import { getPackageInfo, getPackageUsageStats } from './get'; const MockRegistry = Registry as jest.Mocked; +jest.mock('../../settings'); + +const mockGetSettings = getSettings as jest.Mock; +mockGetSettings.mockResolvedValue({ prerelease_integrations_enabled: true }); + describe('When using EPM `get` services', () => { describe('and invoking getPackageUsageStats()', () => { let soClient: jest.Mocked; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index b8b447a8de526..e8d1cd1380303 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -49,8 +49,14 @@ export async function getPackages( excludeInstallStatus?: boolean; } & Registry.SearchParams ) { - const { savedObjectsClient, experimental, category, excludeInstallStatus = false } = options; - const registryItems = await Registry.fetchList({ category, experimental }).then((items) => { + const { + savedObjectsClient, + category, + excludeInstallStatus = false, + prerelease = false, + } = options; + + const registryItems = await Registry.fetchList({ category, prerelease }).then((items) => { return items.map((item) => Object.assign({}, item, { title: item.title || nameAsTitle(item.name) }, { id: item.name }) ); @@ -87,11 +93,12 @@ export async function getPackages( // Get package names for packages which cannot have more than one package policy on an agent policy export async function getLimitedPackages(options: { savedObjectsClient: SavedObjectsClientContract; + prerelease?: boolean; }): Promise { - const { savedObjectsClient } = options; + const { savedObjectsClient, prerelease } = options; const allPackages = await getPackages({ savedObjectsClient, - experimental: true, + prerelease, }); const installedPackages = allPackages.filter( (pkg) => pkg.status === installationStatuses.Installed @@ -126,6 +133,7 @@ export async function getPackageInfo({ pkgVersion, skipArchive = false, ignoreUnverified = false, + prerelease, }: { savedObjectsClient: SavedObjectsClientContract; pkgName: string; @@ -133,10 +141,11 @@ export async function getPackageInfo({ /** Avoid loading the registry archive into the cache (only use for performance reasons). Defaults to `false` */ skipArchive?: boolean; ignoreUnverified?: boolean; + prerelease?: boolean; }): Promise { const [savedObject, latestPackage] = await Promise.all([ getInstallationObject({ savedObjectsClient, pkgName }), - Registry.fetchFindLatestPackageOrUndefined(pkgName), + Registry.fetchFindLatestPackageOrUndefined(pkgName, { prerelease }), ]); if (!savedObject && !latestPackage) { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.ts b/x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.ts new file mode 100644 index 0000000000000..df4b47d13ef2f --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/get_prerelease_setting.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectsClientContract } from '@kbn/core/server'; + +import { appContextService } from '../../app_context'; +import { getSettings } from '../../settings'; + +export async function getPrereleaseFromSettings( + savedObjectsClient: SavedObjectsClientContract +): Promise { + let prerelease: boolean = false; + try { + ({ prerelease_integrations_enabled: prerelease } = await getSettings(savedObjectsClient)); + } catch (err) { + appContextService + .getLogger() + .warn('Error while trying to load prerelease flag from settings, defaulting to false', err); + } + return prerelease; +} 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 ebd0bde8b09b4..d7f67ca1d2ae0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -113,7 +113,7 @@ export async function ensureInstalledPackage(options: { // If pkgVersion isn't specified, find the latest package version const pkgKeyProps = pkgVersion ? { name: pkgName, version: pkgVersion } - : await Registry.fetchFindLatestPackageOrThrow(pkgName); + : await Registry.fetchFindLatestPackageOrThrow(pkgName, { prerelease: true }); const installedPackageResult = await isPackageVersionOrLaterInstalled({ savedObjectsClient, @@ -234,6 +234,7 @@ interface InstallRegistryPackageParams { force?: boolean; neverIgnoreVerificationError?: boolean; ignoreConstraints?: boolean; + prerelease?: boolean; } interface InstallUploadedArchiveParams { savedObjectsClient: SavedObjectsClientContract; @@ -301,6 +302,7 @@ async function installPackageFromRegistry({ const [latestPackage, { paths, packageInfo, verificationResult }] = await Promise.all([ Registry.fetchFindLatestPackageOrThrow(pkgName, { ignoreConstraints, + prerelease: true, }), Registry.getPackage(pkgName, pkgVersion, { ignoreUnverified: force && !neverIgnoreVerificationError, diff --git a/x-pack/plugins/fleet/server/services/epm/registry/index.ts b/x-pack/plugins/fleet/server/services/epm/registry/index.ts index 4213a50ebf5bf..a6259d1eb6552 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/index.ts @@ -25,6 +25,7 @@ import type { GetCategoriesRequest, PackageVerificationResult, ArchivePackage, + BundledPackage, } from '../../../types'; import { getArchiveFilelist, @@ -55,6 +56,8 @@ import { getRegistryUrl } from './registry_url'; export interface SearchParams { category?: CategoryId; + prerelease?: boolean; + // deprecated experimental?: boolean; } @@ -70,8 +73,8 @@ export async function fetchList(params?: SearchParams): Promise { return withPackageSpan(`Find latest package ${packageName}`, async () => { const logger = appContextService.getLogger(); - const { ignoreConstraints = false } = options ?? {}; + const { ignoreConstraints = false, prerelease = false } = options ?? {}; const bundledPackage = await getBundledPackageByName(packageName); + // temporary workaround to allow synthetics package beta version until there is a GA available + // needed because synthetics is installed by default on kibana startup + const prereleaseAllowedExceptions = ['synthetics']; + + const prereleaseEnabled = prerelease || prereleaseAllowedExceptions.includes(packageName); + const registryUrl = getRegistryUrl(); - const url = new URL(`${registryUrl}/search?package=${packageName}&experimental=true`); + const url = new URL( + `${registryUrl}/search?package=${packageName}&prerelease=${prereleaseEnabled}` + ); if (!ignoreConstraints) { setKibanaVersion(url); @@ -224,8 +236,8 @@ export async function fetchCategories( const registryUrl = getRegistryUrl(); const url = new URL(`${registryUrl}/categories`); if (params) { - if (params.experimental) { - url.searchParams.set('experimental', params.experimental.toString()); + if (params.prerelease) { + url.searchParams.set('prerelease', params.prerelease.toString()); } if (params.include_policy_templates) { url.searchParams.set('include_policy_templates', params.include_policy_templates.toString()); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index f52316dd4452b..b4e78d2f4c2ca 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -187,6 +187,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { savedObjectsClient: soClient, pkgName: packagePolicy.package.name, pkgVersion: packagePolicy.package.version, + prerelease: true, })); // Check if it is a limited package, and if so, check that the corresponding agent policy does not @@ -508,6 +509,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { savedObjectsClient: soClient, pkgName: packagePolicy.package.name, pkgVersion: packagePolicy.package.version, + prerelease: true, }); validatePackagePolicyOrThrow(packagePolicy, pkgInfo); @@ -801,6 +803,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { savedObjectsClient: soClient, pkgName: packagePolicy!.package!.name, pkgVersion: pkgVersion ?? '', + prerelease: !!pkgVersion, // using prerelease only if version is specified }); } @@ -1129,6 +1132,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { pkgName, pkgVersion, skipArchive: true, + prerelease: true, }); if (packageInfo) { return packageToPackagePolicy(packageInfo, ''); @@ -1146,6 +1150,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { savedObjectsClient: soClient, pkgName: pkgInstall.name, pkgVersion: pkgInstall.version, + prerelease: true, }); if (packageInfo) { @@ -1594,6 +1599,7 @@ async function getPackageInfoForPackagePolicies( savedObjectsClient: soClient, pkgName: pkgInfo.name, pkgVersion: pkgInfo.version, + prerelease: true, }); resultMap.set(pkgKey, pkgInfoData); diff --git a/x-pack/plugins/fleet/server/services/settings.ts b/x-pack/plugins/fleet/server/services/settings.ts index 5cde2dbf99815..e710251c39f91 100644 --- a/x-pack/plugins/fleet/server/services/settings.ts +++ b/x-pack/plugins/fleet/server/services/settings.ts @@ -99,5 +99,5 @@ function getConfigFleetServerHosts() { } export function createDefaultSettings(): BaseSettings { - return {}; + return { prerelease_integrations_enabled: false }; } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts index f69576a2a8b56..9e36413f80150 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/epm.ts @@ -9,7 +9,8 @@ import { schema } from '@kbn/config-schema'; export const GetCategoriesRequestSchema = { query: schema.object({ - experimental: schema.maybe(schema.boolean()), + prerelease: schema.maybe(schema.boolean()), + experimental: schema.maybe(schema.boolean()), // deprecated include_policy_templates: schema.maybe(schema.boolean()), }), }; @@ -17,11 +18,18 @@ export const GetCategoriesRequestSchema = { export const GetPackagesRequestSchema = { query: schema.object({ category: schema.maybe(schema.string()), - experimental: schema.maybe(schema.boolean()), + prerelease: schema.maybe(schema.boolean()), + experimental: schema.maybe(schema.boolean()), // deprecated excludeInstallStatus: schema.maybe(schema.boolean({ defaultValue: false })), }), }; +export const GetLimitedPackagesRequestSchema = { + query: schema.object({ + prerelease: schema.maybe(schema.boolean()), + }), +}; + export const GetFileRequestSchema = { params: schema.object({ pkgName: schema.string(), @@ -37,6 +45,7 @@ export const GetInfoRequestSchema = { }), query: schema.object({ ignoreUnverified: schema.maybe(schema.boolean()), + prerelease: schema.maybe(schema.boolean()), }), }; @@ -44,6 +53,10 @@ export const GetInfoRequestSchemaDeprecated = { params: schema.object({ pkgkey: schema.string(), }), + query: schema.object({ + ignoreUnverified: schema.maybe(schema.boolean()), + prerelease: schema.maybe(schema.boolean()), + }), }; export const UpdatePackageRequestSchema = { @@ -96,6 +109,9 @@ export const InstallPackageFromRegistryRequestSchemaDeprecated = { }; export const BulkUpgradePackagesFromRegistryRequestSchema = { + query: schema.object({ + prerelease: schema.maybe(schema.boolean()), + }), body: schema.object({ packages: schema.arrayOf(schema.string(), { minSize: 1 }), }), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts index 4544b677cba8c..4a1c2975c80e0 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/settings.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/settings.ts @@ -35,5 +35,6 @@ export const PutSettingsRequestSchema = { }) ), kibana_ca_sha256: schema.maybe(schema.string()), + prerelease_integrations_enabled: schema.maybe(schema.boolean()), }), }; diff --git a/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts b/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts index 29110f5807edf..fe875fe4e2565 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/bulk_upgrade.ts @@ -62,7 +62,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should return 200 and an array for upgrading a package', async function () { const { body }: { body: BulkInstallPackagesResponse } = await supertest - .post(`/api/fleet/epm/packages/_bulk`) + .post(`/api/fleet/epm/packages/_bulk?prerelease=true`) .set('kbn-xsrf', 'xxxx') .send({ packages: ['multiple_versions'] }) .expect(200); @@ -73,7 +73,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should return an error for packages that do not exist', async function () { const { body }: { body: BulkInstallPackagesResponse } = await supertest - .post(`/api/fleet/epm/packages/_bulk`) + .post(`/api/fleet/epm/packages/_bulk?prerelease=true`) .set('kbn-xsrf', 'xxxx') .send({ packages: ['multiple_versions', 'blahblah'] }) .expect(200); @@ -88,7 +88,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should upgrade multiple packages', async function () { const { body }: { body: BulkInstallPackagesResponse } = await supertest - .post(`/api/fleet/epm/packages/_bulk`) + .post(`/api/fleet/epm/packages/_bulk?prerelease=true`) .set('kbn-xsrf', 'xxxx') .send({ packages: ['multiple_versions', 'overrides'] }) .expect(200); @@ -110,7 +110,7 @@ export default function (providerContext: FtrProviderContext) { it('should return 200 and an array for upgrading a package', async function () { const { body }: { body: BulkInstallPackagesResponse } = await supertest - .post(`/api/fleet/epm/packages/_bulk`) + .post(`/api/fleet/epm/packages/_bulk?prerelease=true`) .set('kbn-xsrf', 'xxxx') .send({ packages: ['multiple_versions'] }) .expect(200); diff --git a/x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts index 0ef5f648ab36d..8d7a6323d5f73 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/custom_ingest_pipeline.ts @@ -32,7 +32,7 @@ export default function (providerContext: FtrProviderContext) { // Use the custom log package to test the custom ingest pipeline before(async () => { const { body: getPackagesRes } = await supertest.get( - `/api/fleet/epm/packages?experimental=true` + `/api/fleet/epm/packages?prerelease=true` ); const logPackage = getPackagesRes.items.find((p: any) => p.name === 'log'); if (!logPackage) { diff --git a/x-pack/test/fleet_api_integration/apis/epm/delete.ts b/x-pack/test/fleet_api_integration/apis/epm/delete.ts index 076cc6bba4efc..9ec8b7318f6c9 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/delete.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/delete.ts @@ -34,6 +34,7 @@ export default function (providerContext: FtrProviderContext) { describe('delete and force delete scenarios', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + before(async () => { await installPackage(requiredPackage, pkgVersion); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts index e705aa1734cb0..5e677bd96d54a 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/final_pipeline.ts @@ -43,7 +43,7 @@ export default function (providerContext: FtrProviderContext) { // Use the custom log package to test the fleet final pipeline before(async () => { const { body: getPackagesRes } = await supertest.get( - `/api/fleet/epm/packages?experimental=true` + `/api/fleet/epm/packages?prerelease=true` ); const logPackage = getPackagesRes.items.find((p: any) => p.name === 'log'); if (!logPackage) { diff --git a/x-pack/test/fleet_api_integration/apis/epm/get.ts b/x-pack/test/fleet_api_integration/apis/epm/get.ts index 280922b2e4a33..17f83b0b3c534 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/get.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/get.ts @@ -40,6 +40,7 @@ export default function (providerContext: FtrProviderContext) { describe('EPM - get', () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + it('returns package info from the registry if it was installed from the registry', async function () { // this will install through the registry by default await installPackage(testPkgName, testPkgVersion); @@ -149,7 +150,7 @@ export default function (providerContext: FtrProviderContext) { // not from the package registry. This is because they contain a field the registry // does not support const res = await supertest - .get(`/api/fleet/epm/packages/integration_to_input/0.9.1`) + .get(`/api/fleet/epm/packages/integration_to_input/0.9.1?prerelease=true`) .expect(200); const packageInfo = res.body.item; @@ -158,14 +159,16 @@ export default function (providerContext: FtrProviderContext) { }); describe('Pkg verification', () => { it('should return validation error for unverified input only pkg', async function () { - const res = await supertest.get(`/api/fleet/epm/packages/input_only/0.1.0`).expect(400); + const res = await supertest + .get(`/api/fleet/epm/packages/input_only/0.1.0?prerelease=true`) + .expect(400); const error = res.body; expect(error?.attributes?.type).to.equal('verification_failed'); }); it('should not return validation error for unverified input only pkg if ignoreUnverified is true', async function () { await supertest - .get(`/api/fleet/epm/packages/input_only/0.1.0?ignoreUnverified=true`) + .get(`/api/fleet/epm/packages/input_only/0.1.0?ignoreUnverified=true&prerelease=true`) .expect(200); }); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts index e4fd8f11141ae..d7c913fc8bc4b 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_by_upload.ts @@ -60,6 +60,7 @@ export default function (providerContext: FtrProviderContext) { describe('installs packages from direct upload', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + afterEach(async () => { if (server) { // remove the packages just in case it being installed will affect other tests diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts b/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts index fba01e840cfd0..7649237f0ab4d 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_error_rollback.ts @@ -30,12 +30,15 @@ export default function (providerContext: FtrProviderContext) { }; const getPackageInfo = async (pkg: string, version: string) => { - return await supertest.get(`/api/fleet/epm/packages/${pkg}/${version}`).set('kbn-xsrf', 'xxxx'); + return await supertest + .get(`/api/fleet/epm/packages/${pkg}/${version}?prerelease=true`) + .set('kbn-xsrf', 'xxxx'); }; describe('package installation error handling and rollback', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + beforeEach(async () => { await kibanaServer.savedObjects.cleanStandardList(); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts index d33f9b8a922d9..b89c036e84a9d 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_overrides.ts @@ -26,6 +26,7 @@ export default function (providerContext: FtrProviderContext) { describe('installs packages that include settings and mappings overrides', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + after(async () => { if (server.enabled) { // remove the package just in case it being installed will affect other tests diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts b/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts index f375c9902317f..86e91d6707c5a 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_prerelease.ts @@ -25,6 +25,7 @@ export default function (providerContext: FtrProviderContext) { describe('installs package that has a prerelease version', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + after(async () => { if (server.enabled) { // remove the package just in case it being installed will affect other tests diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_kbn_assets_in_space.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_kbn_assets_in_space.ts index 9e364f28f8b3e..08740ea5335b2 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_kbn_assets_in_space.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_kbn_assets_in_space.ts @@ -50,6 +50,7 @@ export default function (providerContext: FtrProviderContext) { describe('installs and uninstalls all assets (non default space)', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + before(async () => { await createSpace(testSpaceId); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts index 275a2abf744bc..48071d15436fd 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts @@ -62,6 +62,7 @@ export default function (providerContext: FtrProviderContext) { describe('installs and uninstalls multiple packages side effects', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + before(async () => { if (!server.enabled) return; await installPackages([ diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts index 7458912207a38..aca56f5fce936 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_tag_assets.ts @@ -68,6 +68,7 @@ export default function (providerContext: FtrProviderContext) { describe('asset tagging', () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + before(async () => { await createSpace(testSpaceId); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_update.ts b/x-pack/test/fleet_api_integration/apis/epm/install_update.ts index 669166b189789..c36efd6066b6e 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_update.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_update.ts @@ -26,6 +26,7 @@ export default function (providerContext: FtrProviderContext) { describe('installing and updating scenarios', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + after(async () => { await deletePackage('multiple_versions', '0.3.0'); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/list.ts b/x-pack/test/fleet_api_integration/apis/epm/list.ts index 51f003a7192d5..5727f7130f563 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/list.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/list.ts @@ -23,6 +23,7 @@ export default function (providerContext: FtrProviderContext) { describe('EPM - list', async function () { skipIfNoDockerRegistry(providerContext); + before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/fleet/empty_fleet_server'); }); diff --git a/x-pack/test/fleet_api_integration/apis/epm/package_install_complete.ts b/x-pack/test/fleet_api_integration/apis/epm/package_install_complete.ts index fdf90c636885d..f29e36daebdd2 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/package_install_complete.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/package_install_complete.ts @@ -26,6 +26,7 @@ export default function (providerContext: FtrProviderContext) { describe('setup checks packages completed install', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + describe('package install', async () => { before(async () => { if (!server.enabled) return; diff --git a/x-pack/test/fleet_api_integration/apis/epm/setup.ts b/x-pack/test/fleet_api_integration/apis/epm/setup.ts index 7720d915f6f13..f930166f4a74f 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/setup.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/setup.ts @@ -21,6 +21,7 @@ export default function (providerContext: FtrProviderContext) { describe('setup api', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); + // FLAKY: https://github.com/elastic/kibana/issues/118479 describe.skip('setup performs upgrades', async () => { const oldEndpointVersion = '0.13.0'; diff --git a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts index e1e1f90f57eb6..3f76f4594592f 100644 --- a/x-pack/test/fleet_api_integration/apis/fleet_setup.ts +++ b/x-pack/test/fleet_api_integration/apis/fleet_setup.ts @@ -69,7 +69,7 @@ export default function (providerContext: FtrProviderContext) { await supertest.post(`/api/fleet/setup`).set('kbn-xsrf', 'xxxx').expect(200); const { body: apiResponse } = await supertest - .get(`/api/fleet/epm/packages?experimental=true`) + .get(`/api/fleet/epm/packages?prerelease=true`) .expect(200); const installedPackages = apiResponse.response .filter((p: any) => p.status === 'installed') diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts index 383a92fa164e8..b15b84dcfbee9 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/delete.ts @@ -15,6 +15,7 @@ export default function (providerContext: FtrProviderContext) { describe('Package Policy - delete', () => { skipIfNoDockerRegistry(providerContext); + describe('Delete one', () => { let agentPolicy: any; let packagePolicy: any; diff --git a/x-pack/test/fleet_api_integration/helpers.ts b/x-pack/test/fleet_api_integration/helpers.ts index d3b623d0426f3..c21a9e01b3309 100644 --- a/x-pack/test/fleet_api_integration/helpers.ts +++ b/x-pack/test/fleet_api_integration/helpers.ts @@ -89,3 +89,19 @@ export async function generateAgent( refresh: 'wait_for', }); } + +export function setPrereleaseSetting(supertest: any) { + before(async () => { + await supertest + .put('/api/fleet/settings') + .set('kbn-xsrf', 'xxxx') + .send({ prerelease_integrations_enabled: true }); + }); + + after(async () => { + await supertest + .put('/api/fleet/settings') + .set('kbn-xsrf', 'xxxx') + .send({ prerelease_integrations_enabled: false }); + }); +} diff --git a/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts index ebe434b6afe6e..d60d7b89a0121 100644 --- a/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts +++ b/x-pack/test/functional/apps/maps/group4/geofile_wizard_auto_open.ts @@ -22,7 +22,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const geoFileCard = await find.byCssSelector( '[data-test-subj="integration-card:ui_link:ingest_geojson"]' ); - geoFileCard.click(); + await geoFileCard.click(); }); it('should navigate to maps app with url params', async () => { diff --git a/x-pack/test/functional_synthetics/services/uptime/synthetics_package.ts b/x-pack/test/functional_synthetics/services/uptime/synthetics_package.ts index 14c7abaa57343..5b9525bf2060b 100644 --- a/x-pack/test/functional_synthetics/services/uptime/synthetics_package.ts +++ b/x-pack/test/functional_synthetics/services/uptime/synthetics_package.ts @@ -50,7 +50,7 @@ export function SyntheticsPackageProvider({ getService }: FtrProviderContext) { apiRequest = retry.try(() => { return supertest .get(INGEST_API_EPM_PACKAGES) - .query({ experimental: true }) + .query({ prerelease: true }) .set('kbn-xsrf', 'xxx') .expect(200) .catch((error) => {