diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index ae4de55ffa9a88..36972270de011a 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { installationStatuses } from '../constants'; import { PackageInfo } from '../types'; import { packageToPackagePolicy, packageToPackagePolicyInputs } from './package_to_package_policy'; @@ -14,9 +13,9 @@ describe('Fleet - packageToPackagePolicy', () => { version: '0.0.0', latestVersion: '0.0.0', description: 'description', - type: 'mock', + type: 'integration', categories: [], - requirement: { kibana: { versions: '' }, elasticsearch: { versions: '' } }, + conditions: { kibana: { version: '' } }, format_version: '', download: '', path: '', @@ -29,7 +28,11 @@ describe('Fleet - packageToPackagePolicy', () => { map: [], }, }, - status: installationStatuses.NotInstalled, + status: 'not_installed', + release: 'experimental', + owner: { + github: 'elastic/fleet', + }, }; describe('packageToPackagePolicyInputs', () => { diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 0169c0c50f65ac..c99ad71a2df6eb 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -15,6 +15,7 @@ import { requiredPackages, } from '../../constants'; import { ValueOf } from '../../types'; +import { PackageSpecManifest, PackageSpecScreenshot } from './package_spec'; export type InstallationStatus = typeof installationStatuses; @@ -67,40 +68,40 @@ export enum ElasticsearchAssetType { export type DataType = typeof dataTypes; -export type RegistryRelease = 'ga' | 'beta' | 'experimental'; +export type InstallablePackage = RegistryPackage | ArchivePackage; -// Fields common to packages that come from direct upload and the registry -export interface InstallablePackage { - name: string; - title?: string; - version: string; - release?: RegistryRelease; - readme?: string; - description: string; - type: string; - categories: string[]; - screenshots?: RegistryImage[]; - icons?: RegistryImage[]; - assets?: string[]; - internal?: boolean; - format_version: string; - data_streams?: RegistryDataStream[]; - policy_templates?: RegistryPolicyTemplate[]; -} +export type ArchivePackage = PackageSpecManifest & + // should an uploaded package be able to specify `internal`? + Pick; -// Uploaded package archives don't have extra fields -// Linter complaint disabled because this extra type is meant for better code readability -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ArchivePackage extends InstallablePackage {} +export type RegistryPackage = PackageSpecManifest & + Partial & + RegistryAdditionalProperties & + RegistryOverridePropertyValue; // Registry packages do have extra fields. // cf. type Package struct at https://github.com/elastic/package-registry/blob/master/util/package.go -export interface RegistryPackage extends InstallablePackage { - requirement: RequirementsByServiceName; +type RegistryOverridesToOptional = Pick; + +// our current types have `download`, & `path` as required but they're are optional (have `omitempty`) according to +// https://github.com/elastic/package-registry/blob/master/util/package.go#L57 +// & https://github.com/elastic/package-registry/blob/master/util/package.go#L80-L81 +// However, they are always present in every registry response I checked. Chose to keep types unchanged for now +// and confirm with Registry if they are really optional. Can update types and ~4 places in code later if neccessary +interface RegistryAdditionalProperties { + assets?: string[]; download: string; path: string; + readme?: string; + internal?: boolean; // Registry addition[0] and EPM uses it[1] [0]: https://github.com/elastic/package-registry/blob/dd7b021893aa8d66a5a5fde963d8ff2792a9b8fa/util/package.go#L63 [1] + data_streams?: RegistryDataStream[]; // Registry addition [0] [0]: https://github.com/elastic/package-registry/blob/dd7b021893aa8d66a5a5fde963d8ff2792a9b8fa/util/package.go#L65 +} +interface RegistryOverridePropertyValue { + icons?: RegistryImage[]; + screenshots?: RegistryImage[]; } +export type RegistryRelease = PackageSpecManifest['release']; export interface RegistryImage { src: string; path: string; @@ -108,22 +109,22 @@ export interface RegistryImage { size?: string; type?: string; } + export interface RegistryPolicyTemplate { name: string; title: string; description: string; - inputs: RegistryInput[]; + inputs?: RegistryInput[]; multiple?: boolean; } - export interface RegistryInput { type: string; title: string; - description?: string; - vars?: RegistryVarsEntry[]; + description: string; template_path?: string; + condition?: string; + vars?: RegistryVarsEntry[]; } - export interface RegistryStream { input: string; title: string; @@ -152,15 +153,15 @@ export type RegistrySearchResult = Pick< | 'release' | 'description' | 'type' - | 'icons' - | 'internal' | 'download' | 'path' + | 'icons' + | 'internal' | 'data_streams' | 'policy_templates' >; -export type ScreenshotItem = RegistryImage; +export type ScreenshotItem = RegistryImage | PackageSpecScreenshot; // from /categories // https://github.com/elastic/package-registry/blob/master/docs/api/categories.json @@ -172,7 +173,7 @@ export interface CategorySummaryItem { count: number; } -export type RequirementsByServiceName = Record; +export type RequirementsByServiceName = PackageSpecManifest['conditions']; export interface AssetParts { pkgkey: string; dataset?: string; @@ -241,31 +242,24 @@ export interface RegistryVarsEntry { // some properties are optional in Registry responses but required in EPM // internal until we need them -interface PackageAdditions { +export interface EpmPackageAdditions { title: string; latestVersion: string; assets: AssetsGroupedByServiceByType; removable?: boolean; } +type Merge = Omit> & + SecondType; + // Managers public HTTP response types export type PackageList = PackageListItem[]; export type PackageListItem = Installable; export type PackagesGroupedByStatus = Record, PackageList>; export type PackageInfo = - | Installable< - // remove the properties we'll be altering/replacing from the base type - Omit & - // now add our replacement definitions - PackageAdditions - > - | Installable< - // remove the properties we'll be altering/replacing from the base type - Omit & - // now add our replacement definitions - PackageAdditions - >; + | Installable> + | Installable>; export interface Installation extends SavedObjectAttributes { installed_kibana: KibanaAssetReference[]; diff --git a/x-pack/plugins/fleet/common/types/models/index.ts b/x-pack/plugins/fleet/common/types/models/index.ts index ad4c6ad02639e2..80b7cd0026c8e1 100644 --- a/x-pack/plugins/fleet/common/types/models/index.ts +++ b/x-pack/plugins/fleet/common/types/models/index.ts @@ -10,5 +10,6 @@ export * from './package_policy'; export * from './data_stream'; export * from './output'; export * from './epm'; +export * from './package_spec'; export * from './enrollment_api_key'; export * from './settings'; diff --git a/x-pack/plugins/fleet/common/types/models/package_spec.ts b/x-pack/plugins/fleet/common/types/models/package_spec.ts new file mode 100644 index 00000000000000..9c83865a913842 --- /dev/null +++ b/x-pack/plugins/fleet/common/types/models/package_spec.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RegistryPolicyTemplate } from './epm'; + +// Based on https://github.com/elastic/package-spec/blob/master/versions/1/manifest.spec.yml#L8 +export interface PackageSpecManifest { + format_version: string; + name: string; + title: string; + description: string; + version: string; + license?: 'basic'; + type?: 'integration'; + release: 'experimental' | 'beta' | 'ga'; + categories?: Array; + conditions?: PackageSpecConditions; + icons?: PackageSpecIcon[]; + screenshots?: PackageSpecScreenshot[]; + policy_templates?: RegistryPolicyTemplate[]; + owner: { github: string }; +} + +export type PackageSpecCategory = + | 'aws' + | 'azure' + | 'cloud' + | 'config_management' + | 'containers' + | 'crm' + | 'custom' + | 'datastore' + | 'elastic_stack' + | 'google_cloud' + | 'kubernetes' + | 'languages' + | 'message_queue' + | 'monitoring' + | 'network' + | 'notification' + | 'os_system' + | 'productivity' + | 'security' + | 'support' + | 'ticketing' + | 'version_control' + | 'web'; + +export type PackageSpecConditions = Record< + 'kibana', + { + version: string; + } +>; + +export interface PackageSpecIcon { + src: string; + title?: string; + size?: string; + type?: string; +} + +export interface PackageSpecScreenshot { + src: string; + title: string; + size?: string; + type?: string; +} 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 0709eddaa52ec9..7299fbb5e5d656 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/epm.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/epm.ts @@ -8,7 +8,7 @@ import { AssetReference, CategorySummaryList, Installable, - RegistryPackage, + RegistrySearchResult, PackageInfo, } from '../models/epm'; @@ -30,14 +30,7 @@ export interface GetPackagesRequest { } export interface GetPackagesResponse { - response: Array< - Installable< - Pick< - RegistryPackage, - 'name' | 'title' | 'version' | 'description' | 'type' | 'icons' | 'download' | 'path' - > - > - >; + response: Array>; } export interface GetLimitedPackagesResponse { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx index 3d6cd2bc61e72d..a2529159f32f73 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/components/requirements.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import React, { Fragment } from 'react'; import styled from 'styled-components'; -import { RequirementsByServiceName, entries } from '../../../types'; +import { RequirementsByServiceName, ServiceName, entries } from '../../../types'; import { ServiceTitleMap } from '../constants'; import { Version } from './version'; @@ -23,6 +23,14 @@ const StyledVersion = styled(Version)` font-size: ${(props) => props.theme.eui.euiFontSizeXS}; `; +// both our custom `entries` and this type seem unnecessary & duplicate effort but they work for now +type RequirementEntry = [ + Extract, + { + version: string; + } +]; + export function Requirements(props: RequirementsProps) { const { requirements } = props; @@ -40,7 +48,7 @@ export function Requirements(props: RequirementsProps) { - {entries(requirements).map(([service, requirement]) => ( + {entries(requirements).map(([service, requirement]: RequirementEntry) => ( @@ -48,7 +56,7 @@ export function Requirements(props: RequirementsProps) { - + ))} diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx index 1dad25e9cf0595..fe5390e75f6a1a 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/constants.tsx @@ -29,8 +29,7 @@ export const AssetTitleMap: Record = { map: 'Map', }; -export const ServiceTitleMap: Record = { - elasticsearch: 'Elasticsearch', +export const ServiceTitleMap: Record, string> = { kibana: 'Kibana', }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx index 08165332806d31..5299f4f9be8bc1 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/hooks/use_links.tsx @@ -6,7 +6,7 @@ import { useStartServices } from '../../../hooks/use_core'; import { PLUGIN_ID } from '../../../constants'; import { epmRouteService } from '../../../services'; -import { RegistryImage } from '../../../../../../common'; +import { PackageSpecIcon, PackageSpecScreenshot, RegistryImage } from '../../../../../../common'; const removeRelativePath = (relativePath: string): string => new URL(relativePath, 'http://example.com').pathname; @@ -15,12 +15,19 @@ export function useLinks() { const { http } = useStartServices(); return { toAssets: (path: string) => http.basePath.prepend(`/plugins/${PLUGIN_ID}/assets/${path}`), - toPackageImage: (img: RegistryImage, pkgName: string, pkgVersion: string): string => - img.src - ? http.basePath.prepend( - epmRouteService.getFilePath(`/package/${pkgName}/${pkgVersion}${img.src}`) - ) - : http.basePath.prepend(epmRouteService.getFilePath(img.path)), + toPackageImage: ( + img: PackageSpecIcon | PackageSpecScreenshot | RegistryImage, + pkgName: string, + pkgVersion: string + ): string | undefined => { + const sourcePath = img.src + ? `/package/${pkgName}/${pkgVersion}${img.src}` + : 'path' in img && img.path; + if (sourcePath) { + const filePath = epmRouteService.getFilePath(sourcePath); + return http.basePath.prepend(filePath); + } + }, toRelativeImage: ({ path, packageName, diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx index 09089902115ba1..31e35ee43b42f7 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/epm/screens/detail/screenshots.tsx @@ -46,6 +46,7 @@ export function Screenshots(props: ScreenshotProps) { // for now, just get first image const image = images[0]; const hasCaption = image.title ? true : false; + const screenshotUrl = toPackageImage(image, packageName, version); return ( @@ -69,18 +70,20 @@ export function Screenshots(props: ScreenshotProps) { )} - - {/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images, + {screenshotUrl && ( + + {/* By default EuiImage sets width to 100% and Figure to 22.5rem for size=l images, set image to same width. Will need to update if size changes. */} - - + + + )} ); diff --git a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts index 47688f55fbc085..cf5e7ae0f063c7 100644 --- a/x-pack/plugins/fleet/server/services/epm/archive/validation.ts +++ b/x-pack/plugins/fleet/server/services/epm/archive/validation.ts @@ -40,20 +40,23 @@ const requiredArchivePackageProps: readonly RequiredPackageProp[] = [ 'name', 'version', 'description', - 'type', - 'categories', + 'title', 'format_version', + 'release', + 'owner', ] as const; const optionalArchivePackageProps: readonly OptionalPackageProp[] = [ - 'title', - 'release', 'readme', - 'screenshots', - 'icons', 'assets', - 'internal', 'data_streams', + 'internal', + 'license', + 'type', + 'categories', + 'conditions', + 'screenshots', + 'icons', 'policy_templates', ] as const; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index b7650d10b6b250..6d97cbc83d2aa3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -63,12 +63,16 @@ describe('_installPackage', () => { callCluster, paths: [], packageInfo: { + title: 'title', name: 'xyz', version: '4.5.6', description: 'test', - type: 'x', - categories: ['this', 'that'], + type: 'integration', + categories: ['cloud', 'custom'], format_version: 'string', + release: 'experimental', + conditions: { kibana: { version: 'x.y.z' } }, + owner: { github: 'elastic/fleet' }, }, installType: 'install', installSource: 'registry', 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 9b4b26d6fb8b38..0d7006ca41d2bb 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -7,7 +7,12 @@ import { SavedObjectsClientContract, SavedObjectsFindOptions } from 'src/core/server'; import { isPackageLimited, installationStatuses } from '../../../../common'; import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; -import { ArchivePackage, InstallSource, RegistryPackage } from '../../../../common/types'; +import { + ArchivePackage, + InstallSource, + RegistryPackage, + EpmPackageAdditions, +} from '../../../../common/types'; import { Installation, PackageInfo, KibanaAssetType } from '../../../types'; import * as Registry from '../registry'; import { createInstallableFrom, isRequiredPackage } from './index'; @@ -107,13 +112,14 @@ export async function getPackageInfo(options: { const packageInfo = getPackageRes.packageInfo; // add properties that aren't (or aren't yet) on the package - const updated = { - ...packageInfo, + const additions: EpmPackageAdditions = { latestVersion: latestPackage.version, title: packageInfo.title || nameAsTitle(packageInfo.name), assets: Registry.groupPathsByService(paths || []), removable: !isRequiredPackage(pkgName), }; + const updated = { ...packageInfo, ...additions }; + return createInstallableFrom(updated, savedObject); } diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 3c508bed5b2f1b..4e001554226ff9 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -1298,7 +1298,7 @@ export class EndpointDocGenerator { title: 'Elastic Endpoint', version: '0.5.0', description: 'This is the Elastic Endpoint package.', - type: 'solution', + type: 'integration', download: '/epr/endpoint/endpoint-0.5.0.tar.gz', path: '/package/endpoint/0.5.0', icons: [ @@ -1310,6 +1310,7 @@ export class EndpointDocGenerator { }, ], status: 'installed', + release: 'ga', savedObject: { type: 'epm-packages', id: 'endpoint', 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 885386b092108d..7eab90a4b4b07d 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 @@ -157,7 +157,7 @@ export default function (providerContext: FtrProviderContext) { .send(buf) .expect(400); expect(res.error.text).to.equal( - '{"statusCode":400,"error":"Bad Request","message":"Invalid top-level package manifest: one or more fields missing of name, version, description, type, categories, format_version"}' + '{"statusCode":400,"error":"Bad Request","message":"Invalid top-level package manifest: one or more fields missing of name, version, description, title, format_version, release, owner"}' ); }); diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip index 8526f6a53458b6..fa329e57ec44f8 100644 Binary files a/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip and b/x-pack/test/fleet_api_integration/apis/fixtures/direct_upload_packages/apache_invalid_manifest_missing_field_0.1.4.zip differ