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 support for forcing stack aligned versions on bundled packages #139567

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion fleet_packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
on disk rather than only in the configured package registry service. This allows Kibana to install
"stack-aligned" packages or those that are installed by default in an airgapped or offline environment.

The `forceAlignStackVersion` option is available for packages who wish to opt into having their
versions forcibly rewritten to the current version of Kibana. This is useful for packages that
deploy multiple, version-aligned stack components like APM. When this option is enabled, Kibana
will fetch the latest available version of the package from EPR (including prerelease versions),
download that version, and rewrite its version to align with Kibana's.

Packages will be fetched from https://epr-snapshot.elastic.co by default. This can be overridden
via the `--epr-registry=production` command line argument when building Kibana. Fetching from the
snapshot registry allows Kibana to bundle packages that have yet to be published to production in
Expand All @@ -14,7 +20,8 @@
[
{
"name": "apm",
"version": "8.4.0"
"version": "8.4.0",
"forceAlignStackVersion": true
},
{
"name": "elastic_agent",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,8 @@
"xml2js": "^0.4.22",
"xterm": "^4.18.0",
"xterm-addon-fit": "^0.5.0",
"yauzl": "^2.10.0"
"yauzl": "^2.10.0",
"yazl": "^2.5.1"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^10.0.3",
Expand Down Expand Up @@ -1164,6 +1165,7 @@
"@types/xml2js": "^0.4.5",
"@types/yargs": "^15.0.0",
"@types/yauzl": "^2.9.1",
"@types/yazl": "^2.4.2",
"@types/zen-observable": "^0.8.0",
"@typescript-eslint/eslint-plugin": "^5.20.0",
"@typescript-eslint/parser": "^5.20.0",
Expand Down
93 changes: 93 additions & 0 deletions src/dev/build/lib/archive_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import fs from 'fs';
import yauzl from 'yauzl';
import yazl from 'yazl';

// The utilities in this file are largely copied with minor modifications from
jbudz marked this conversation as resolved.
Show resolved Hide resolved
// `x-pack/plugins/fleet/server/services/epm/extract.ts`. We can't import them directly
// in the bundled package task due to tsconfig limitation, so they're copied here instead.

export interface ZipEntry {
path: string;
buffer?: Buffer;
}

export async function unzipBuffer(buffer: Buffer): Promise<ZipEntry[]> {
const zipEntries: ZipEntry[] = [];
const zipfile = await yauzlFromBuffer(buffer, { lazyEntries: true });

zipfile.readEntry();
zipfile.on('entry', async (entry: yauzl.Entry) => {
const path = entry.fileName;

// Only include files, not directories
if (path.endsWith('/')) {
return zipfile.readEntry();
}

const entryBuffer = await getZipReadStream(zipfile, entry).then(streamToBuffer);
zipEntries.push({ buffer: entryBuffer, path });

zipfile.readEntry();
});

await new Promise((resolve, reject) => zipfile.on('end', resolve).on('error', reject));

return zipEntries;
}

export async function createZipFile(entries: ZipEntry[], destination: string): Promise<Buffer> {
const zipfile = new yazl.ZipFile();

for (const entry of entries) {
zipfile.addBuffer(entry.buffer || Buffer.from(''), entry.path);
}

return new Promise((resolve, reject) => {
zipfile.outputStream.on('error', reject);

zipfile.end();

zipfile.outputStream
.pipe(fs.createWriteStream(destination))
.on('close', resolve)
.on('error', reject);
});
}

// Copied over some utilities from x-pack/plugins/fleet/server/services/epm/archive/extract.ts since we can't
// import them directly due to `tsconfig` limitations in the `kibana/src/` directory.
function yauzlFromBuffer(buffer: Buffer, opts: yauzl.Options): Promise<yauzl.ZipFile> {
return new Promise((resolve, reject) =>
yauzl.fromBuffer(buffer, opts, (err?: Error, handle?: yauzl.ZipFile) =>
err ? reject(err) : resolve(handle!)
)
);
}

function getZipReadStream(
zipfile: yauzl.ZipFile,
entry: yauzl.Entry
): Promise<NodeJS.ReadableStream> {
return new Promise((resolve, reject) =>
zipfile.openReadStream(entry, (err?: Error, readStream?: NodeJS.ReadableStream) =>
err ? reject(err) : resolve(readStream!)
)
);
}

function streamToBuffer(stream: NodeJS.ReadableStream): Promise<Buffer> {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
stream.on('end', () => resolve(Buffer.concat(chunks)));
stream.on('error', reject);
});
}
1 change: 1 addition & 0 deletions src/dev/build/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from './scan_delete';
export * from './scan_copy';
export * from './platform';
export * from './scan';
export * from './archive_utils';
64 changes: 57 additions & 7 deletions src/dev/build/tasks/bundle_fleet_packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,18 @@
*/

import JSON5 from 'json5';
import fs from 'fs/promises';
import { safeLoad, safeDump } from 'js-yaml';

import { readCliArgs } from '../args';
import { Task, read, downloadToDisk } from '../lib';
import { Task, read, downloadToDisk, unzipBuffer, createZipFile } from '../lib';

const BUNDLED_PACKAGES_DIR = 'x-pack/plugins/fleet/target/bundled_packages';

interface FleetPackage {
name: string;
version: string;
forceAlignStackVersion?: boolean;
}

export const BundleFleetPackages: Task = {
Expand All @@ -25,8 +28,7 @@ export const BundleFleetPackages: Task = {
log.info('Fetching fleet packages from package registry');
log.indent(4);

// Support the `--use-snapshot-epr` command line argument to fetch from the snapshot registry
// in development or test environments
// Support the `--epr-registry` command line argument to fetch from the snapshot or production registry
const { buildOptions } = readCliArgs(process.argv);
const eprUrl =
buildOptions?.eprRegistry === 'snapshot'
Expand All @@ -40,13 +42,28 @@ export const BundleFleetPackages: Task = {

log.debug(
`Found configured bundled packages: ${parsedFleetPackages
.map((fleetPackage) => `${fleetPackage.name}-${fleetPackage.version}`)
.map((fleetPackage) => `${fleetPackage.name}-${fleetPackage.version || 'latest'}`)
.join(', ')}`
);

await Promise.all(
parsedFleetPackages.map(async (fleetPackage) => {
const archivePath = `${fleetPackage.name}-${fleetPackage.version}.zip`;
const stackVersion = config.getBuildVersion();

let versionToWrite = fleetPackage.version;

// If `forceAlignStackVersion` is set, we will rewrite the version specified in the config
// to the version of the stack when writing the bundled package to disk. This allows us
// to support some unique package development workflows, e.g. APM.
if (fleetPackage.forceAlignStackVersion) {
versionToWrite = stackVersion;

log.debug(
`Bundling ${fleetPackage.name}-${fleetPackage.version} as ${fleetPackage.name}-${stackVersion} to align with stack version`
);
}

const archivePath = `${fleetPackage.name}-${versionToWrite}.zip`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can support 2 endpoints temporarily. The job I'm building here will use indices behind https://epr-v2.ea-web.elastic.dev/, which are different from https://epr-snapshot.elastic.co/ at the moment.

We have 3 options:

  1. We will enable the job once we switch to Package Storage v2.
  2. build_fleet_packages.ts will check also the v2 endpoint.
  3. The job can update the fleet_packages.json with direct links to packages (and signatures).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep Kibana somewhat unaware of a separate package storage v2 endpoint if possible.

What is your thinking behind checking v1 and v2? Just trying to catch cases where a package hasn't been upload to both places yet when a build occurs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually it's more tangled.

APM pipeline is integrated only with v2, so you will never see this package in v1: https://epr-v2.ea-web.elastic.dev/search?prerelease=true&package=apm (8.5.0-preview-1661950351).

That's why I was thinking about setting download_urls in fleet_packages.json... or we will enable the job once we switch to v2 (epr-snapshot will point to https://epr-v2.ea-web.elastic.dev/).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in favor of enabling the job once we switch to v2 to keep Kibana's assumption around EPR endpoints accurate before and after the release of package storage v2.

const archiveUrl = `${eprUrl}/epr/${fleetPackage.name}/${fleetPackage.name}-${fleetPackage.version}.zip`;

const destination = build.resolvePath(BUNDLED_PACKAGES_DIR, archivePath);
Expand All @@ -61,9 +78,42 @@ export const BundleFleetPackages: Task = {
skipChecksumCheck: true,
maxAttempts: 3,
});

// If we're force aligning the version, we need to
// 1. Unzip the downloaded archive
// 2. Edit the `manifest.yml` file to include the updated `version` value
// 3. Re-zip the archive and replace it on disk
if (fleetPackage.forceAlignStackVersion) {
const buffer = await fs.readFile(destination);
const zipEntries = await unzipBuffer(buffer);

const manifestPath = `${fleetPackage.name}-${fleetPackage.version}/manifest.yml`;
const manifestEntry = zipEntries.find((entry) => entry.path === manifestPath);

if (!manifestEntry || !manifestEntry.buffer) {
log.debug(
`Unable to find manifest.yml for stack aligned package ${fleetPackage.name}`
);

return;
}

const manifestYml = await safeLoad(manifestEntry.buffer.toString('utf8'));
manifestYml.version = stackVersion;

const newManifestYml = safeDump(manifestYml);
manifestEntry.buffer = Buffer.from(newManifestYml, 'utf8');

// Update all paths to use the new version
zipEntries.forEach(
(entry) => (entry.path = entry.path.replace(fleetPackage.version, versionToWrite!))
);

await createZipFile(zipEntries, destination);
}
} catch (error) {
log.warning(`Failed to download bundled package archive ${archivePath}`);
log.warning(error);
log.error(`Failed to download bundled package archive ${archivePath}`);
throw error;
}
})
);
Expand Down
21 changes: 15 additions & 6 deletions x-pack/plugins/fleet/server/services/epm/packages/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ interface InstallUploadedArchiveParams {
archiveBuffer: Buffer;
contentType: string;
spaceId: string;
version?: string;
}

function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent {
Expand Down Expand Up @@ -437,6 +438,7 @@ async function installPackageByUpload({
archiveBuffer,
contentType,
spaceId,
version,
}: InstallUploadedArchiveParams): Promise<InstallResult> {
// Workaround apm issue with async spans: https://github.com/elastic/apm-agent-nodejs/issues/2611
await Promise.resolve();
Expand All @@ -449,21 +451,27 @@ async function installPackageByUpload({
try {
const { packageInfo } = await generatePackageInfoFromArchiveBuffer(archiveBuffer, contentType);

// Allow for overriding the version in the manifest for cases where we install
// stack-aligned bundled packages. In general, the version pulled from a bundled package
// filename and the version in the manifest will match, except for special cases around the
// `forceAlignStackVersion` flag in `fleet_packages.json`.
kpollich marked this conversation as resolved.
Show resolved Hide resolved
const pkgVersion = version || packageInfo.version;

const installedPkg = await getInstallationObject({
savedObjectsClient,
pkgName: packageInfo.name,
});

installType = getInstallType({ pkgVersion: packageInfo.version, installedPkg });
installType = getInstallType({ pkgVersion, installedPkg });

span?.addLabels({
packageName: packageInfo.name,
packageVersion: packageInfo.version,
packageVersion: pkgVersion,
installType,
});

telemetryEvent.packageName = packageInfo.name;
telemetryEvent.newVersion = packageInfo.version;
telemetryEvent.newVersion = pkgVersion;
telemetryEvent.installType = installType;
telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed';

Expand All @@ -472,14 +480,14 @@ async function installPackageByUpload({
deleteVerificationResult(packageInfo);
const paths = await unpackBufferToCache({
name: packageInfo.name,
version: packageInfo.version,
version: pkgVersion,
archiveBuffer,
contentType,
});

setPackageInfo({
name: packageInfo.name,
version: packageInfo.version,
version: pkgVersion,
packageInfo,
});

Expand All @@ -505,7 +513,7 @@ async function installPackageByUpload({
logger,
installedPkg,
paths,
packageInfo,
packageInfo: { ...packageInfo, version: pkgVersion },
installType,
installSource,
spaceId,
Expand Down Expand Up @@ -572,6 +580,7 @@ export async function installPackage(args: InstallPackageParams): Promise<Instal
archiveBuffer: matchingBundledPackage.buffer,
contentType: 'application/zip',
spaceId,
version: matchingBundledPackage.version,
});

return { ...response, installSource: 'bundled' };
Expand Down
7 changes: 7 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8738,6 +8738,13 @@
dependencies:
"@types/node" "*"

"@types/yazl@^2.4.2":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@types/yazl/-/yazl-2.4.2.tgz#d5f8a4752261badbf1a36e8b49e042dc18ec84bc"
integrity sha512-T+9JH8O2guEjXNxqmybzQ92mJUh2oCwDDMSSimZSe1P+pceZiFROZLYmcbqkzV5EUwz6VwcKXCO2S2yUpra6XQ==
dependencies:
"@types/node" "*"

"@types/zen-observable@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d"
Expand Down