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] Allow bundled installs to occur even if EPR is unreachable #125127

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
1d75cc6
Allow bundled installs to occur even if EPR is unreachable
kpollich Feb 9, 2022
68ad69a
Fix type errors in test
kpollich Feb 9, 2022
c69c92a
Fix failing test
kpollich Feb 9, 2022
c09df4f
fixup! Fix failing test
kpollich Feb 9, 2022
8056bba
Remove unused object in mock
kpollich Feb 9, 2022
aba45c7
Make creation of preconfigured agent policy functional
kpollich Feb 10, 2022
928b7ef
Always fall back to bundled packages if available
kpollich Feb 10, 2022
13e218d
Remove unused import
kpollich Feb 10, 2022
e2f2fea
Use packageInfo object instead of RegistryPackage where possible
kpollich Feb 10, 2022
bac2d46
Fix type error in assets test
kpollich Feb 10, 2022
f833166
Fix test timeouts
kpollich Feb 10, 2022
bdcc878
Fix promise logic for registry fetch fallback
kpollich Feb 10, 2022
e23ddbb
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kpollich Feb 10, 2022
af1d13c
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kpollich Feb 11, 2022
49be525
Use archive package as default in create package policy
kpollich Feb 11, 2022
f165e5d
Always install from bundled package if it exists - regardless of inst…
kpollich Feb 11, 2022
132b81d
Clean up + refactor a bit
kpollich Feb 11, 2022
ecfa6fa
Default to cached package archive for policy updates
kpollich Feb 11, 2022
b4d244c
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kibanamachine Feb 14, 2022
8634894
Update mock in get.test.ts
kpollich Feb 14, 2022
80f3e65
Add test for install from bundled package logic
kpollich Feb 14, 2022
1f2dfe3
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kibanamachine Feb 14, 2022
61fa10e
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kibanamachine Feb 14, 2022
670d24c
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kpollich Feb 14, 2022
8f0ad33
Delete timeout call in security solution tests
kpollich Feb 14, 2022
84751f1
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kibanamachine Feb 15, 2022
d192922
Fix unused var in endpoint test
kpollich Feb 15, 2022
840c4ec
Fix another unused var in endpoint test
kpollich Feb 15, 2022
513dcc1
[Debug] Add some logging to test installation times in CI
kpollich Feb 15, 2022
21baf09
Revert "[Debug] Add some logging to test installation times in CI"
kpollich Feb 15, 2022
d79e967
Update docker images for registry
kpollich Feb 15, 2022
cce17e3
Merge branch 'main' into 125097-prevent-registry-network-errors-block…
kpollich Feb 15, 2022
2310ec8
Update docker image digest again
kpollich Feb 15, 2022
53f3b60
Refactor latest package fetching to fix broken logic/tests
kpollich Feb 15, 2022
883e216
Fix a bunch of type errors around renamed fetch latest package versio…
kpollich Feb 15, 2022
3541e4f
Remove unused import
kpollich Feb 15, 2022
3a828d5
Bump docker version to latest snapshot (again)
kpollich Feb 16, 2022
9e7b122
Revert changes to endpoint tests
kpollich Feb 16, 2022
351ce75
Pass experimental flag in synthetics tests
kpollich Feb 16, 2022
7938cd7
Fix endpoint version in fleet api integration test
kpollich Feb 16, 2022
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
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export type InstallablePackage = RegistryPackage | ArchivePackage;

export type ArchivePackage = PackageSpecManifest &
// should an uploaded package be able to specify `internal`?
Pick<RegistryPackage, 'readme' | 'assets' | 'data_streams' | 'internal'>;
Pick<RegistryPackage, 'readme' | 'assets' | 'data_streams' | 'internal' | 'elasticsearch'>;

export type RegistryPackage = PackageSpecManifest &
Partial<RegistryOverridesToOptional> &
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class ConcurrentInstallOperationError extends IngestManagerError {}
export class AgentReassignmentError extends IngestManagerError {}
export class PackagePolicyIneligibleForUpgradeError extends IngestManagerError {}
export class PackagePolicyValidationError extends IngestManagerError {}
export class BundledPackageNotFoundError extends IngestManagerError {}
export class HostedAgentPolicyRestrictionRelatedError extends IngestManagerError {
constructor(message = 'Cannot perform that action') {
super(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export async function getFullAgentPolicy(
options?: { standalone: boolean }
): Promise<FullAgentPolicy | null> {
let agentPolicy;
const standalone = options?.standalone;
const standalone = options?.standalone ?? false;

try {
agentPolicy = await agentPolicyService.get(soClient, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import * as Registry from '../registry';

import type { InstallResult } from '../../../types';

import { BundledPackageNotFoundError } from '../../../errors';

import { installPackage, isPackageVersionOrLaterInstalled } from './install';
import type { BulkInstallResponse, IBulkInstallPackageError } from './install';
import { getBundledPackages } from './get_bundled_packages';
Expand Down Expand Up @@ -39,8 +41,32 @@ export async function bulkInstallPackages({

const packagesResults = await Promise.allSettled(
packagesToInstall.map((pkg) => {
if (typeof pkg === 'string') return Registry.fetchFindLatestPackage(pkg);
return Promise.resolve(pkg);
if (typeof pkg !== 'string') {
return Promise.resolve(pkg);
}

return Registry.fetchFindLatestPackage(pkg).catch((error) => {
// For registry preferred bulk installs, consider an inability to reach the configured
// registry a fotal error
if (preferredSource === 'registry') {
return Promise.reject(error);
}

// For bundled preferred bulk installs, we want to allow the installation to continue, even
// if the registry is not reachable
const bundledPackage = bundledPackages.find((b) => b.name === pkg);

if (!bundledPackage) {
return Promise.reject(
new BundledPackageNotFoundError(`No bundled package found with name ${pkg}`)
);
}

return Promise.resolve({
name: bundledPackage.name,
version: bundledPackage.version,
});
});
})
);

Expand Down Expand Up @@ -86,7 +112,7 @@ export async function bulkInstallPackages({
let installResult: InstallResult;
const pkgkey = Registry.pkgToPkgKey(pkgKeyProps);

const bundledPackage = bundledPackages.find((pkg) => pkg.name === pkgkey);
const bundledPackage = bundledPackages.find((pkg) => `${pkg.name}-${pkg.version}` === pkgkey);

// If preferred source is bundled packages on disk, attempt to install from disk first, then fall back to registry
if (preferredSource === 'bundled') {
Expand Down
22 changes: 20 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 @@ -21,14 +21,15 @@ import type {
GetCategoriesRequest,
} from '../../../../common/types';
import type { Installation, PackageInfo } from '../../../types';
import { IngestManagerError } from '../../../errors';
import { BundledPackageNotFoundError, IngestManagerError } from '../../../errors';
import { appContextService } from '../../';
import * as Registry from '../registry';
import { getEsPackage } from '../archive/storage';
import { getArchivePackage } from '../archive';
import { normalizeKuery } from '../../saved_object';

import { createInstallableFrom } from './index';
import { getBundledPackages } from './get_bundled_packages';

export type { SearchParams } from '../registry';
export { getFile } from '../registry';
Expand Down Expand Up @@ -106,7 +107,24 @@ export async function getPackageInfo(options: {
const { savedObjectsClient, pkgName, pkgVersion } = options;
const [savedObject, latestPackage] = await Promise.all([
getInstallationObject({ savedObjectsClient, pkgName }),
Registry.fetchFindLatestPackage(pkgName),
new Promise<{ name: string; version: string }>((resolve, reject) => {
return Registry.fetchFindLatestPackage(pkgName).catch(async (error) => {
const bundledPackages = await getBundledPackages();
const bundledPackage = bundledPackages.find(
(b) => b.name === pkgName && b.version === pkgVersion
);

// If we don't find a bundled package, reject with the original error
if (!bundledPackage) {
return reject(error);
}

resolve({
name: bundledPackage.name,
version: bundledPackage.version,
});
});
}),
]);

// If no package version is provided, use the installed version in the response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import path from 'path';
import fs from 'fs/promises';

import { appContextService } from '../../app_context';
import { splitPkgKey } from '../registry';

const BUNDLED_PACKAGE_DIRECTORY = path.join(__dirname, '../../../bundled_packages');

interface BundledPackage {
name: string;
version: string;
buffer: Buffer;
}

Expand All @@ -26,8 +28,11 @@ export async function getBundledPackages(): Promise<BundledPackage[]> {
zipFiles.map(async (zipFile) => {
const file = await fs.readFile(path.join(BUNDLED_PACKAGE_DIRECTORY, zipFile));

const { pkgName, pkgVersion } = splitPkgKey(zipFile.replace(/\.zip$/, ''));

return {
name: zipFile.replace(/\.zip$/, ''),
name: pkgName,
version: pkgVersion,
buffer: file,
};
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function fetchFindLatestPackage(packageName: string): Promise<Regis

setKibanaVersion(url);

const res = await fetchUrl(url.toString());
const res = await fetchUrl(url.toString(), 1);
const searchResults = JSON.parse(res);
if (searchResults.length) {
return searchResults[0];
Expand Down
15 changes: 9 additions & 6 deletions x-pack/plugins/fleet/server/services/epm/registry/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ async function registryFetch(url: string) {
}
}

export async function getResponse(url: string): Promise<Response> {
export async function getResponse(url: string, retries: number = 5): Promise<Response> {
try {
// we only want to retry certain failures like network issues
// the rest should only try the one time then fail as they do now
const response = await pRetry(() => registryFetch(url), {
factor: 2,
retries: 5,
retries,
onFailedAttempt: (error) => {
// we only want to retry certain types of errors, like `ECONNREFUSED` and other operational errors
// and let the others through without retrying
Expand All @@ -67,13 +67,16 @@ export async function getResponse(url: string): Promise<Response> {
}
}

export async function getResponseStream(url: string): Promise<NodeJS.ReadableStream> {
const res = await getResponse(url);
export async function getResponseStream(
url: string,
retries?: number
): Promise<NodeJS.ReadableStream> {
const res = await getResponse(url, retries);
return res.body;
}

export async function fetchUrl(url: string): Promise<string> {
return getResponseStream(url).then(streamToString);
export async function fetchUrl(url: string, retries?: number): Promise<string> {
return getResponseStream(url, retries).then(streamToString);
}

// node-fetch throws a FetchError for those types of errors and
Expand Down
57 changes: 43 additions & 14 deletions x-pack/plugins/fleet/server/services/package_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
ListResult,
UpgradePackagePolicyDryRunResponseItem,
RegistryDataStream,
InstallablePackage,
} from '../../common';
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants';
import {
Expand All @@ -57,7 +58,6 @@ import type {
UpdatePackagePolicy,
PackagePolicy,
PackagePolicySOAttributes,
RegistryPackage,
DryRunPackagePolicy,
} from '../types';
import type { ExternalCallback } from '..';
Expand All @@ -73,6 +73,7 @@ import { appContextService } from '.';
import { removeOldAssets } from './epm/packages/cleanup';
import type { PackageUpdateEvent, UpdateEventType } from './upgrade_sender';
import { sendTelemetryEvents } from './upgrade_sender';
import { getArchivePackage } from './epm/archive';

export type InputsOverride = Partial<NewPackagePolicyInput> & {
vars?: Array<NewPackagePolicyInput['vars'] & { name: string }>;
Expand Down Expand Up @@ -134,7 +135,8 @@ class PackagePolicyService {
pkgVersion: packagePolicy.package.version,
});

let pkgInfo;
let pkgInfo: PackageInfo;

if (options?.skipEnsureInstalled) pkgInfo = await pkgInfoPromise;
else {
const [, packageInfo] = await Promise.all([
Expand Down Expand Up @@ -162,16 +164,33 @@ class PackagePolicyService {
}
validatePackagePolicyOrThrow(packagePolicy, pkgInfo);

const registryPkgInfo = await Registry.fetchInfo(pkgInfo.name, pkgInfo.version);
const installablePackage: InstallablePackage = await Registry.fetchInfo(
Copy link
Member

Choose a reason for hiding this comment

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

Here the package should be installed right? can we directly fetch from ES the package instead of relying on the registry or the bundled package?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah I believe you are correct. I'm not sure why, but all of the policy compilation logic expected a RegistryPackage object. I would've assumed we would be fine with PackageInfo here and so we could fetch from ES. I will take a look at this. You're right above that it's non-trivial but this is probably the "right" solution.

Copy link
Member Author

Choose a reason for hiding this comment

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

So it wound up being pretty easy to replace the InstallablePackage stuff with a PackageInfo object - just needed to make a few changes here and there to support that. We only use a few fields here, and getPackageInfo will pull back an Installation object for installed packages, so I think we've pretty easily eliminated the registry call here. See e2f2fea

pkgInfo.name,
pkgInfo.version
).catch(async (error) => {
// Attempt to get an ArchivePackage object from the package info cache if the registry
// is not reachable. This will pull a bundled package if it was previously installed.
const archivePackage = await getArchivePackage({
kpollich marked this conversation as resolved.
Show resolved Hide resolved
name: pkgInfo.name,
version: pkgInfo.version,
});

// If we don't find a package in the cache, throw the original error
if (!archivePackage) {
throw error;
}

return archivePackage?.packageInfo;
});

inputs = await this._compilePackagePolicyInputs(
registryPkgInfo,
installablePackage,
pkgInfo,
packagePolicy.vars || {},
inputs
);

elasticsearch = registryPkgInfo.elasticsearch;
elasticsearch = installablePackage.elasticsearch;
}

const isoDate = new Date().toISOString();
Expand Down Expand Up @@ -798,14 +817,24 @@ class PackagePolicyService {
}

public async _compilePackagePolicyInputs(
registryPkgInfo: RegistryPackage,
installablePackage: InstallablePackage,
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
inputs: PackagePolicyInput[]
): Promise<PackagePolicyInput[]> {
const inputsPromises = inputs.map(async (input) => {
const compiledInput = await _compilePackagePolicyInput(registryPkgInfo, pkgInfo, vars, input);
const compiledStreams = await _compilePackageStreams(registryPkgInfo, pkgInfo, vars, input);
const compiledInput = await _compilePackagePolicyInput(
installablePackage,
pkgInfo,
vars,
input
);
const compiledStreams = await _compilePackageStreams(
installablePackage,
pkgInfo,
vars,
input
);
return {
...input,
compiled_input: compiledInput,
Expand Down Expand Up @@ -916,7 +945,7 @@ function assignStreamIdToInput(packagePolicyId: string, input: NewPackagePolicyI
}

async function _compilePackagePolicyInput(
registryPkgInfo: RegistryPackage,
installablePackage: InstallablePackage,
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
input: PackagePolicyInput
Expand All @@ -941,7 +970,7 @@ async function _compilePackagePolicyInput(
return undefined;
}

const [pkgInputTemplate] = await getAssetsData(registryPkgInfo, (path: string) =>
const [pkgInputTemplate] = await getAssetsData(installablePackage, (path: string) =>
path.endsWith(`/agent/input/${packageInput.template_path!}`)
);

Expand All @@ -957,13 +986,13 @@ async function _compilePackagePolicyInput(
}

async function _compilePackageStreams(
registryPkgInfo: RegistryPackage,
installablePackage: InstallablePackage,
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
input: PackagePolicyInput
) {
const streamsPromises = input.streams.map((stream) =>
_compilePackageStream(registryPkgInfo, pkgInfo, vars, input, stream)
_compilePackageStream(installablePackage, pkgInfo, vars, input, stream)
);

return await Promise.all(streamsPromises);
Expand Down Expand Up @@ -1006,7 +1035,7 @@ export function _applyIndexPrivileges(
}

async function _compilePackageStream(
registryPkgInfo: RegistryPackage,
installablePackage: InstallablePackage,
pkgInfo: PackageInfo,
vars: PackagePolicy['vars'],
input: PackagePolicyInput,
Expand Down Expand Up @@ -1049,7 +1078,7 @@ async function _compilePackageStream(
const datasetPath = packageDataStream.path;

const [pkgStreamTemplate] = await getAssetsData(
registryPkgInfo,
installablePackage,
(path: string) => path.endsWith(streamFromPkg.template_path),
datasetPath
);
Expand Down
10 changes: 4 additions & 6 deletions x-pack/plugins/fleet/server/services/preconfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,12 +164,6 @@ jest.mock('./epm/packages/install', () => ({
// Treat the buffer value passed in tests as the package's name for simplicity
const pkgName = archiveBuffer.toString('utf8');

const installedPackage = mockInstalledPackages.get(pkgName);

if (installedPackage) {
return installedPackage;
}

// Just install every bundled package at version '1.0.0'
const packageInstallation = { name: pkgName, version: '1.0.0', title: pkgName };
mockInstalledPackages.set(pkgName, packageInstallation);
Expand Down Expand Up @@ -743,11 +737,13 @@ describe('policy preconfiguration', () => {
mockedGetBundledPackages.mockResolvedValue([
{
name: 'test_package',
version: '1.0.0',
buffer: Buffer.from('test_package'),
},

{
name: 'test_package_2',
version: '1.0.0',
buffer: Buffer.from('test_package_2'),
},
]);
Expand Down Expand Up @@ -784,6 +780,7 @@ describe('policy preconfiguration', () => {
mockedGetBundledPackages.mockResolvedValue([
{
name: 'test_package',
version: '1.0.0',
buffer: Buffer.from('test_package'),
},
]);
Expand Down Expand Up @@ -823,6 +820,7 @@ describe('policy preconfiguration', () => {
mockedGetBundledPackages.mockResolvedValue([
{
name: 'test_package',
version: '1.0.0',
buffer: Buffer.from('test_package'),
},
]);
Expand Down