Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Add ?full option to get package info endpoint to return all package fields #144343

Merged
merged 13 commits into from
Nov 8, 2022
8 changes: 8 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,14 @@
"name": "ignoreUnverified",
"description": "Ignore if the package is fails signature verification",
"in": "query"
},
{
"schema": {
"type": "boolean"
},
"name": "full",
"description": "Return all fields from the package manifest, not just those supported by the Elastic Package Registry",
"in": "query"
}
],
"post": {
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/fleet/common/openapi/bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,13 @@ paths:
name: ignoreUnverified
description: Ignore if the package is fails signature verification
in: query
- schema:
type: boolean
name: full
description: >-
Return all fields from the package manifest, not just those supported
by the Elastic Package Registry
in: query
post:
summary: Packages - Install
tags: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ parameters:
name: ignoreUnverified
description: 'Ignore if the package is fails signature verification'
in: query
- schema:
type: boolean
name: full
description: 'Return all fields from the package manifest, not just those supported by the Elastic Package Registry'
in: query
post:
summary: Packages - Install
tags: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const CreatePackagePolicyMultiPage: CreatePackagePolicyParams = ({
data: packageInfoData,
error: packageInfoError,
isLoading: isPackageInfoLoading,
} = useGetPackageInfoByKey(pkgName, pkgVersion, { prerelease: true });
} = useGetPackageInfoByKey(pkgName, pkgVersion, { prerelease: true, full: true });

const {
agentPolicy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const CreatePackagePolicySinglePage: CreatePackagePolicyParams = ({
data: packageInfoData,
error: packageInfoError,
isLoading: isPackageInfoLoading,
} = useGetPackageInfoByKey(pkgName, pkgVersion, { prerelease: true });
} = useGetPackageInfoByKey(pkgName, pkgVersion, { full: true, prerelease: true });
const packageInfo = useMemo(() => {
if (packageInfoData && packageInfoData.item) {
return packageInfoData.item;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ export const EditPackagePolicyForm = memo<{
const { data: packageData } = await sendGetPackageInfoByKey(
_packageInfo!.name,
_packageInfo!.version,
{ prerelease: true }
{ prerelease: true, full: true }
);

if (packageData?.item) {
Expand Down
4 changes: 3 additions & 1 deletion x-pack/plugins/fleet/public/hooks/use_request/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const useGetPackageInfoByKey = (
options?: {
ignoreUnverified?: boolean;
prerelease?: boolean;
full?: boolean;
}
) => {
const confirmOpenUnverified = useConfirmOpenUnverified();
Expand All @@ -96,7 +97,7 @@ export const useGetPackageInfoByKey = (
method: 'get',
query: {
...options,
...(ignoreUnverifiedQueryParam ? { ignoreUnverified: ignoreUnverifiedQueryParam } : {}),
...(ignoreUnverifiedQueryParam && { ignoreUnverified: ignoreUnverifiedQueryParam }),
},
});

Expand Down Expand Up @@ -130,6 +131,7 @@ export const sendGetPackageInfoByKey = (
options?: {
ignoreUnverified?: boolean;
prerelease?: boolean;
full?: boolean;
}
) => {
return sendRequest<GetInfoResponse>({
Expand Down
128 changes: 128 additions & 0 deletions x-pack/plugins/fleet/scripts/get_all_packages/get_all_packages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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 fetch from 'node-fetch';
import { kibanaPackageJson } from '@kbn/utils';
import { ToolingLog } from '@kbn/tooling-log';
import { chunk } from 'lodash';

import yargs from 'yargs/yargs';

import type { PackageInfo } from '../../common';

const REGISTRY_URL = 'https://epr-snapshot.elastic.co';
const KIBANA_URL = 'http://localhost:5601';
const KIBANA_USERNAME = 'elastic';
const KIBANA_PASSWORD = 'changeme';
const KIBANA_VERSION = kibanaPackageJson.version;

const { base = '', prerelease = false, batchSize = 1 } = yargs(process.argv).argv;

const logger = new ToolingLog({
level: 'info',
writeTo: process.stdout,
});

interface Result {
pkg: string;
epr: number;
archive: number;
archiveCached: number;
}
async function getPackage(name: string, version: string, full: boolean = false) {
const start = Date.now();
const res = await fetch(
`${KIBANA_URL}${base}/api/fleet/epm/packages/${name}/${version}?prerelease=true${
full ? '&full=true' : ''
}`,
{
headers: {
accept: '*/*',
'content-type': 'application/json',
'kbn-xsrf': 'xyz',
Authorization:
'Basic ' + Buffer.from(`${KIBANA_USERNAME}:${KIBANA_PASSWORD}`).toString('base64'),
},
method: 'GET',
}
);
const end = Date.now();

let body;
try {
body = await res.json();
} catch (e) {
logger.error(`Error parsing response: ${e}`);
throw e;
}

if (body.item && body.item.name) {
return { pkg: body.item, status: body.status, took: (end - start) / 1000 };
}

throw new Error(`Invalid package returned for ${name}-${version} : ${JSON.stringify(res)}`);
}

async function getAllPackages() {
const res = await fetch(
`${REGISTRY_URL}/search?kibana.version=${KIBANA_VERSION}${
prerelease ? '&prerelease=true' : ''
}`,
{
headers: {
accept: '*/*',
},
method: 'GET',
}
);
const body = await res.json();
return body as PackageInfo[];
}

async function performTest({ name, version }: { name: string; version: string }): Promise<Result> {
const eprResult = await getPackage(name, version);
const archiveResult = await getPackage(name, version, true);
const cachedArchiveResult = await getPackage(name, version, true);
logger.info(`✅ ${name}-${version}`);

return {
pkg: `${name}-${version}`,
epr: eprResult.took,
archive: archiveResult.took,
archiveCached: cachedArchiveResult.took,
};
}

export async function run() {
const allPackages = await getAllPackages();

const batches = chunk(allPackages, batchSize as number);
let allResults: Result[] = [];

const start = Date.now();
for (const batch of batches) {
const results = await Promise.all(batch.map(performTest));
allResults = [...allResults, ...(results.filter((v) => v) as Result[])];
}
const end = Date.now();
const took = (end - start) / 1000;
allResults.sort((a, b) => b.archive - a.archive);
logger.info(`Took ${took} seconds to get ${allResults.length} packages`);
logger.info(
'Average EPM time: ' + allResults.reduce((acc, { epr }) => acc + epr, 0) / allResults.length
);
logger.info(
'Average Archive time: ' +
allResults.reduce((acc, { archive }) => acc + archive, 0) / allResults.length
);
logger.info(
'Average Cache time: ' +
allResults.reduce((acc, { archiveCached }) => acc + archiveCached, 0) / allResults.length
);
// eslint-disable-next-line no-console
console.table(allResults);
}
9 changes: 9 additions & 0 deletions x-pack/plugins/fleet/scripts/get_all_packages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

require('../../../../../src/setup_node_env');
require('./get_all_packages').run();
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/server/routes/epm/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,15 @@ export const getInfoHandler: FleetRequestHandler<
try {
const savedObjectsClient = (await context.fleet).epm.internalSoClient;
const { pkgName, pkgVersion } = request.params;
const { ignoreUnverified = false, prerelease } = request.query;
const { ignoreUnverified = false, full = false, prerelease } = request.query;
if (pkgVersion && !semverValid(pkgVersion)) {
throw new FleetError('Package version is not a valid semver');
}
const res = await getPackageInfo({
savedObjectsClient,
pkgName,
pkgVersion: pkgVersion || '',
skipArchive: true,
skipArchive: !full,
ignoreUnverified,
prerelease,
});
Expand Down
5 changes: 5 additions & 0 deletions x-pack/plugins/fleet/server/services/epm/archive/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export const getArchivePackage = (args: SharedKey) => {
};
};

/*
* This cache should only be used to store "full" package info generated from the package archive.
* NOT package info from the EPR API. This is because we parse extra fields from the archive
* which are not provided by the registry API.
*/
export const setPackageInfo = ({
name,
version,
Expand Down
10 changes: 8 additions & 2 deletions x-pack/plugins/fleet/server/services/epm/packages/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,14 @@ export async function getPackageFromSource(options: {
}
}
} else {
res = await Registry.getPackage(pkgName, pkgVersion, { ignoreUnverified });
logger.debug(`retrieved package ${pkgName}-${pkgVersion} from registry`);
res = getArchivePackage({ name: pkgName, version: pkgVersion });

if (res) {
logger.debug(`retrieved package ${pkgName}-${pkgVersion} from cache`);
} else {
res = await Registry.getPackage(pkgName, pkgVersion, { ignoreUnverified });
logger.debug(`retrieved package ${pkgName}-${pkgVersion} from registry`);
}
}
if (!res) {
throw new FleetError(`package info for ${pkgName}-${pkgVersion} does not exist`);
Expand Down
14 changes: 2 additions & 12 deletions x-pack/plugins/fleet/server/services/epm/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,7 @@ export async function fetchCategories(

export async function getInfo(name: string, version: string) {
return withPackageSpan('Fetch package info', async () => {
let packageInfo = getPackageInfo({ name, version });
if (!packageInfo) {
packageInfo = await fetchInfo(name, version);
// only cache registry pkg info for integration pkgs because
// input type packages must get their pkg info from the archive
if (packageInfo.type === 'integration') setPackageInfo({ name, version, packageInfo });
}

const packageInfo = await fetchInfo(name, version);
return packageInfo as RegistryPackage;
});
}
Expand All @@ -272,15 +265,12 @@ async function getPackageInfoFromArchiveOrCache(
archivePath: string
): Promise<ArchivePackage> {
const cachedInfo = getPackageInfo({ name, version });

if (!cachedInfo) {
const { packageInfo } = await generatePackageInfoFromArchiveBuffer(
archiveBuffer,
ensureContentType(archivePath)
);
// set the download URL as it isn't contained in the manifest
// this allows us to re-download the archive during package install
setPackageInfo({ packageInfo: { ...packageInfo, download: archivePath }, name, version });
setPackageInfo({ packageInfo, name, version });
juliaElastic marked this conversation as resolved.
Show resolved Hide resolved
return packageInfo;
} else {
return cachedInfo;
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/types/rest_spec/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const GetInfoRequestSchema = {
query: schema.object({
ignoreUnverified: schema.maybe(schema.boolean()),
prerelease: schema.maybe(schema.boolean()),
full: schema.maybe(schema.boolean()),
}),
};

Expand Down
36 changes: 36 additions & 0 deletions x-pack/test/fleet_api_integration/apis/epm/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import expect from '@kbn/expect';
import { PackageInfo } from '@kbn/fleet-plugin/common/types/models/epm';
import fs from 'fs';
import path from 'path';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
Expand Down Expand Up @@ -172,5 +173,40 @@ export default function (providerContext: FtrProviderContext) {
.expect(200);
});
});
it('returns package info from the archive if ?full=true', async function () {
const res = await supertest
.get(`/api/fleet/epm/packages/non_epr_fields/1.0.0?full=true`)
.expect(200);
const packageInfo = res.body.item as PackageInfo;
expect(packageInfo?.data_streams?.length).equal(3);
const dataStream = packageInfo?.data_streams?.find(
({ dataset }) => dataset === 'non_epr_fields.test_metrics_2'
);
expect(dataStream?.elasticsearch?.source_mode).equal('default');
});
it('returns package info from the registry if ?full=false', async function () {
const res = await supertest
.get(`/api/fleet/epm/packages/non_epr_fields/1.0.0?full=false`)
.expect(200);
const packageInfo = res.body.item as PackageInfo;
expect(packageInfo?.data_streams?.length).equal(3);
const dataStream = packageInfo?.data_streams?.find(
({ dataset }) => dataset === 'non_epr_fields.test_metrics_2'
);
// this field is only returned if we go to the archive
// it is not part of the EPR API
expect(dataStream?.elasticsearch?.source_mode).equal(undefined);
});
it('returns package info from the registry if ?full not provided', async function () {
const res = await supertest
.get(`/api/fleet/epm/packages/non_epr_fields/1.0.0?full=false`)
.expect(200);
const packageInfo = res.body.item as PackageInfo;
expect(packageInfo?.data_streams?.length).equal(3);
const dataStream = packageInfo?.data_streams?.find(
({ dataset }) => dataset === 'non_epr_fields.test_metrics_2'
);
expect(dataStream?.elasticsearch?.source_mode).equal(undefined);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
- name: logs_test_name
title: logs_test_title
type: text
- name: new_field_name
title: new_field_title
type: keyword
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- name: data_stream.type
type: constant_keyword
description: >
Data stream type.
- name: data_stream.dataset
type: constant_keyword
description: >
Data stream dataset.
- name: data_stream.namespace
type: constant_keyword
description: >
Data stream namespace.
- name: '@timestamp'
type: date
description: >
Event timestamp.