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 18 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 @@ -16,6 +16,7 @@ import type {
RegistryElasticsearch,
InstallablePackage,
IndexTemplate,
PackageInfo,
} from '../../../../types';
import { loadFieldsFromYaml, processFields } from '../../fields/field';
import type { Field } from '../../fields/field';
Expand All @@ -31,6 +32,8 @@ import type { ESAssetMetadata } from '../meta';
import { getESAssetMetadata } from '../meta';
import { retryTransientEsErrors } from '../retry';

import { getPackageInfo } from '../../packages';

import {
generateMappings,
generateTemplateName,
Expand Down Expand Up @@ -62,10 +65,16 @@ export const installTemplates = async (
const dataStreams = installablePackage.data_streams;
if (!dataStreams) return [];

const packageInfo = await getPackageInfo({
savedObjectsClient,
pkgName: installablePackage.name,
pkgVersion: installablePackage.version,
});

const installedTemplatesNested = await Promise.all(
dataStreams.map((dataStream) =>
installTemplateForDataStream({
pkg: installablePackage,
pkg: packageInfo,
esClient,
logger,
dataStream,
Expand Down Expand Up @@ -177,7 +186,7 @@ export async function installTemplateForDataStream({
logger,
dataStream,
}: {
pkg: InstallablePackage;
pkg: PackageInfo;
esClient: ElasticsearchClient;
logger: Logger;
dataStream: RegistryDataStream;
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/server/services/epm/fields/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import { safeLoad } from 'js-yaml';

import type { InstallablePackage } from '../../../types';
import type { PackageInfo } from '../../../types';
import { getAssetsData } from '../packages/assets';

// This should become a copy of https://github.com/elastic/beats/blob/d9a4c9c240a9820fab15002592e5bb6db318543b/libbeat/mapping/field.go#L39
Expand Down Expand Up @@ -261,7 +261,7 @@ const isFields = (path: string) => {
*/

export const loadFieldsFromYaml = async (
pkg: InstallablePackage,
pkg: PackageInfo,
datasetName?: string
): Promise<Field[]> => {
// Fetch all field definition files
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import type { InstallablePackage } from '../../../types';
import type { PackageInfo } from '../../../types';

import { getArchiveFilelist } from '../archive/cache';

Expand Down Expand Up @@ -66,7 +66,7 @@ const tests = [
test('testGetAssets', () => {
for (const value of tests) {
// as needed to pretend it is an InstallablePackage
const assets = getAssets(value.package as InstallablePackage, value.filter, value.dataset);
const assets = getAssets(value.package as PackageInfo, value.filter, value.dataset);
expect(assets).toStrictEqual(value.expected);
}
});
6 changes: 3 additions & 3 deletions x-pack/plugins/fleet/server/services/epm/packages/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import type { InstallablePackage } from '../../../types';
import type { PackageInfo } from '../../../types';
import { getArchiveFilelist, getAsset } from '../archive';
import type { ArchiveEntry } from '../archive';

Expand All @@ -17,7 +17,7 @@ import type { ArchiveEntry } from '../archive';
// and different package and version structure

export function getAssets(
packageInfo: InstallablePackage,
packageInfo: PackageInfo,
filter = (path: string): boolean => true,
datasetName?: string
): string[] {
Expand Down Expand Up @@ -52,7 +52,7 @@ export function getAssets(
// ASK: Does getAssetsData need an installSource now?
// if so, should it be an Installation vs InstallablePackage or add another argument?
export async function getAssetsData(
packageInfo: InstallablePackage,
packageInfo: PackageInfo,
filter = (path: string): boolean => true,
datasetName?: string
): Promise<ArchiveEntry[]> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type { InstallResult } from '../../../types';

import { installPackage, isPackageVersionOrLaterInstalled } from './install';
import type { BulkInstallResponse, IBulkInstallPackageError } from './install';
import { getBundledPackages } from './get_bundled_packages';

interface BulkInstallPackagesParams {
savedObjectsClient: SavedObjectsClientContract;
Expand All @@ -31,23 +30,23 @@ export async function bulkInstallPackages({
esClient,
spaceId,
force,
preferredSource = 'registry',
}: BulkInstallPackagesParams): Promise<BulkInstallResponse[]> {
const logger = appContextService.getLogger();

const bundledPackages = await getBundledPackages();

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

return Registry.fetchFindLatestPackageWithFallbackToBundled(pkg);
})
);

logger.debug(
`kicking off bulk install of ${packagesToInstall.join(
', '
)} with preferred source of "${preferredSource}"`
`kicking off bulk install of ${packagesToInstall
.map((pkg) => (typeof pkg === 'string' ? pkg : pkg.name))
.join(', ')}`
);

const bulkInstallResults = await Promise.allSettled(
Expand Down Expand Up @@ -83,61 +82,16 @@ export async function bulkInstallPackages({
};
}

let installResult: InstallResult;
const pkgkey = Registry.pkgToPkgKey(pkgKeyProps);

const bundledPackage = bundledPackages.find((pkg) => pkg.name === pkgkey);

// If preferred source is bundled packages on disk, attempt to install from disk first, then fall back to registry
if (preferredSource === 'bundled') {
if (bundledPackage) {
logger.debug(
`kicking off install of ${pkgKeyProps.name}-${pkgKeyProps.version} from bundled package on disk`
);
installResult = await installPackage({
savedObjectsClient,
esClient,
installSource: 'upload',
archiveBuffer: bundledPackage.buffer,
contentType: 'application/zip',
spaceId,
});
} else {
installResult = await installPackage({
savedObjectsClient,
esClient,
pkgkey,
installSource: 'registry',
spaceId,
force,
});
}
} else {
// If preferred source is registry, attempt to install from registry first, then fall back to bundled packages on disk
installResult = await installPackage({
savedObjectsClient,
esClient,
pkgkey,
installSource: 'registry',
spaceId,
force,
});

// If we initially errored, try to install from bundled package on disk
if (installResult.error && bundledPackage) {
logger.debug(
`kicking off install of ${pkgKeyProps.name}-${pkgKeyProps.version} from bundled package on disk`
);
installResult = await installPackage({
savedObjectsClient,
esClient,
installSource: 'upload',
archiveBuffer: bundledPackage.buffer,
contentType: 'application/zip',
spaceId,
});
}
}
const installResult = await installPackage({
savedObjectsClient,
esClient,
pkgkey,
installSource: 'registry',
spaceId,
force,
});

if (installResult.error) {
return {
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/fleet/server/services/epm/packages/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,9 +143,10 @@ export async function getPackageInfo(options: {
pkgVersion: string;
}): Promise<PackageInfo> {
const { savedObjectsClient, pkgName, pkgVersion } = options;

const [savedObject, latestPackage] = await Promise.all([
getInstallationObject({ savedObjectsClient, pkgName }),
Registry.fetchFindLatestPackage(pkgName),
Registry.fetchFindLatestPackageWithFallbackToBundled(pkgName),
]);

// 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
24 changes: 24 additions & 0 deletions x-pack/plugins/fleet/server/services/epm/packages/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { removeInstallation } from './remove';
import { getPackageSavedObjects } from './get';
import { _installPackage } from './_install_package';
import { removeOldAssets } from './cleanup';
import { getBundledPackages } from './get_bundled_packages';

export async function isPackageInstalled(options: {
savedObjectsClient: SavedObjectsClientContract;
Expand Down Expand Up @@ -468,8 +469,31 @@ export async function installPackage(args: InstallPackageParams) {
const logger = appContextService.getLogger();
const { savedObjectsClient, esClient } = args;

const bundledPackages = await getBundledPackages();

if (args.installSource === 'registry') {
const { pkgkey, force, spaceId } = args;

const matchingBundledPackage = bundledPackages.find(
Copy link
Member Author

Choose a reason for hiding this comment

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

@joshdover - I made this change to push the "fall back to bundled packages" logic down closer to the place we do the actual installation. The challenging part is the abstraction level of the actual installPackage function - it accept two signatures: one for registry packages and another for uploads. So, the choice I made here was to simply always try to install a given package from its bundled equivalent if it exists.

e.g. if fleet_server-1.0.1 is provided, and we have that exact version included as a bundled package, we'll install from that source even if the function was called with installSource: registry in its arguments. I think this is a fine implementation choice and the only minor drawback I can think of would be snapshot packages where we have a bundled version downloaded to disk, but the version in the snapshot registry has been updated in-place. I think that's a minor enough edge case to warrant this choice.

Let me know if I can clarify anything else here - I'd like to get your thoughts on this approach before merging. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome, thanks for exploring this. I like this implementation much more. I'm ok with this drawback as well. One other minor drawback is that API requests to install a package at latest will not work, but I don't think we support this today anyways so it should be considered separately.

Could we add a test to install.test.ts for this case?

(pkg) => Registry.pkgToPkgKey(pkg) === pkgkey
);

if (matchingBundledPackage) {
logger.debug(
`found bundled package for requested install of ${pkgkey} - installing from bundled package archive`
);

const response = installPackageByUpload({
savedObjectsClient,
esClient,
archiveBuffer: matchingBundledPackage.buffer,
contentType: 'application/zip',
spaceId,
});

return response;
}

logger.debug(`kicking off install of ${pkgkey} from registry`);
const response = installPackageFromRegistry({
savedObjectsClient,
Expand Down
21 changes: 20 additions & 1 deletion x-pack/plugins/fleet/server/services/epm/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { streamToBuffer } from '../streams';
import { appContextService } from '../..';
import { PackageNotFoundError, PackageCacheError, RegistryResponseError } from '../../../errors';

import { getBundledPackages } from '../packages/get_bundled_packages';

import { fetchUrl, getResponse, getResponseStream } from './requests';
import { getRegistryUrl } from './registry_url';

Expand Down Expand Up @@ -71,7 +73,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 All @@ -80,6 +82,23 @@ export async function fetchFindLatestPackage(packageName: string): Promise<Regis
}
}

export async function fetchFindLatestPackageWithFallbackToBundled(pkgName: string) {
return fetchFindLatestPackage(pkgName).catch(async (error) => {
const bundledPackages = await getBundledPackages();
const bundledPackage = bundledPackages.find((b) => b.name === pkgName);

// If we don't find a bundled package, re-throw the original error
if (!bundledPackage) {
throw error;
}

return {
name: bundledPackage.name,
version: bundledPackage.version,
};
});
}

export async function fetchInfo(pkgName: string, pkgVersion: string): Promise<RegistryPackage> {
const registryUrl = getRegistryUrl();
try {
Expand Down
Loading