Skip to content
78 changes: 56 additions & 22 deletions x-pack/plugins/fleet/server/errors/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ import {
PackagePolicyNotFoundError,
FleetUnauthorizedError,
PackagePolicyNameExistsError,
PackageOutdatedError,
PackageInvalidArchiveError,
BundledPackageLocationNotFoundError,
PackageRemovalError,
PackageESError,
KibanaSOReferenceError,
PackageAlreadyInstalledError,
} from '.';

type IngestErrorHandler = (
Expand All @@ -47,45 +54,72 @@ interface IngestErrorHandlerParams {
}
// unsure if this is correct. would prefer to use something "official"
// this type is based on BadRequest values observed while debugging https://github.com/elastic/kibana/issues/75862

const getHTTPResponseCode = (error: FleetError): number => {
if (error instanceof RegistryResponseError) {
// 4xx/5xx's from EPR
return 500;
// Bad Request
if (error instanceof PackageFailedVerificationError) {
return 400;
}
if (error instanceof RegistryConnectionError || error instanceof RegistryError) {
// Connection errors (ie. RegistryConnectionError) / fallback (RegistryError) from EPR
return 502; // Bad Gateway
if (error instanceof PackageOutdatedError) {
return 400;
}
if (error instanceof PackageNotFoundError || error instanceof PackagePolicyNotFoundError) {
return 404; // Not Found
if (error instanceof PackageInvalidArchiveError) {
return 400;
}
if (error instanceof AgentPolicyNameExistsError) {
return 409; // Conflict
if (error instanceof PackageRemovalError) {
return 400;
}
if (error instanceof PackageUnsupportedMediaTypeError) {
return 415; // Unsupported Media Type
if (error instanceof KibanaSOReferenceError) {
return 400;
}
if (error instanceof PackageFailedVerificationError) {
return 400; // Bad Request
// Unauthorized
if (error instanceof FleetUnauthorizedError) {
return 403;
Comment thread
jsoriano marked this conversation as resolved.
}
if (error instanceof ConcurrentInstallOperationError) {
return 409; // Conflict
// Not Found
if (error instanceof PackageNotFoundError || error instanceof PackagePolicyNotFoundError) {
return 404;
}
if (error instanceof AgentNotFoundError) {
return 404;
}
if (error instanceof AgentActionNotFoundError) {
return 404;
}
if (error instanceof FleetUnauthorizedError) {
return 403; // Unauthorized
// Conflict
if (error instanceof AgentPolicyNameExistsError) {
return 409;
}
if (error instanceof ConcurrentInstallOperationError) {
return 409;
}
if (error instanceof PackagePolicyNameExistsError) {
return 409; // Conflict
return 409;
}
if (error instanceof PackageAlreadyInstalledError) {
return 409;
}
// Unsupported Media Type
if (error instanceof PackageUnsupportedMediaTypeError) {
return 415;
}
// Internal Server Error
if (error instanceof UninstallTokenError) {
return 500; // Internal Error
return 500;
}
if (error instanceof BundledPackageLocationNotFoundError) {
return 500;
}
if (error instanceof PackageESError) {
return 500;
}
if (error instanceof RegistryResponseError) {
// 4xx/5xx's from EPR
return 500;
Comment thread
jsoriano marked this conversation as resolved.
}
// Bad Gateway
if (error instanceof RegistryConnectionError || error instanceof RegistryError) {
// Connection errors (ie. RegistryConnectionError) / fallback (RegistryError) from EPR
return 502;
}
return 400; // Bad Request
};
Expand Down Expand Up @@ -115,7 +149,7 @@ export function fleetErrorToResponseOptions(error: IngestErrorHandlerParams['err
};
}

// not sure what type of error this is. log as much as possible
// default response is 500
logger.error(error);
return {
statusCode: 500,
Expand Down
18 changes: 11 additions & 7 deletions x-pack/plugins/fleet/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ export class RegistryResponseError extends RegistryError {
super(message);
}
}

// Package errors
export class PackageNotFoundError extends FleetError {}
export class PackageKeyInvalidError extends FleetError {}
export class PackageOutdatedError extends FleetError {}
export class PackageFailedVerificationError extends FleetError {
constructor(pkgName: string, pkgVersion: string) {
Expand All @@ -37,22 +38,25 @@ export class PackageFailedVerificationError extends FleetError {
};
}
}
export class PackageUnsupportedMediaTypeError extends FleetError {}
export class PackageInvalidArchiveError extends FleetError {}
export class PackageRemovalError extends FleetError {}
export class PackageESError extends FleetError {}
export class ConcurrentInstallOperationError extends FleetError {}
export class BundledPackageLocationNotFoundError extends FleetError {}
export class KibanaSOReferenceError extends FleetError {}
export class PackageAlreadyInstalledError extends FleetError {}

export class AgentPolicyError extends FleetError {}
export class AgentPolicyNotFoundError extends FleetError {}
export class AgentNotFoundError extends FleetError {}
export class AgentActionNotFoundError extends FleetError {}
export class AgentPolicyNameExistsError extends AgentPolicyError {}
export class PackageUnsupportedMediaTypeError extends FleetError {}
export class PackageInvalidArchiveError extends FleetError {}
export class PackageCacheError extends FleetError {}
export class PackageOperationNotSupportedError extends FleetError {}
export class ConcurrentInstallOperationError extends FleetError {}
export class AgentReassignmentError extends FleetError {}
export class PackagePolicyIneligibleForUpgradeError extends FleetError {}
export class PackagePolicyValidationError extends FleetError {}
export class PackagePolicyNameExistsError extends FleetError {}
export class PackagePolicyNotFoundError extends FleetError {}
export class BundledPackageNotFoundError extends FleetError {}
export class HostedAgentPolicyRestrictionRelatedError extends FleetError {
constructor(message = 'Cannot perform that action') {
super(
Expand Down
15 changes: 10 additions & 5 deletions x-pack/plugins/fleet/server/services/epm/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ import { safeLoad, safeDump } from 'js-yaml';

import type { PackagePolicyConfigRecord } from '../../../../common/types';
import { toCompiledSecretRef } from '../../secrets';
import { PackageInvalidArchiveError } from '../../../errors';

const handlebars = Handlebars.create();

export function compileTemplate(variables: PackagePolicyConfigRecord, templateStr: string) {
const { vars, yamlValues } = buildTemplateVariables(variables, templateStr);
const { vars, yamlValues } = buildTemplateVariables(variables);
let compiledTemplate: string;
try {
const template = handlebars.compile(templateStr, { noEscape: true });
compiledTemplate = template(vars);
} catch (err) {
throw new Error(`Error while compiling agent template: ${err.message}`);
throw new PackageInvalidArchiveError(`Error while compiling agent template: ${err.message}`);
}

compiledTemplate = replaceRootLevelYamlVariables(yamlValues, compiledTemplate);
Expand Down Expand Up @@ -64,21 +65,25 @@ function replaceVariablesInYaml(yamlVariables: { [k: string]: any }, yaml: any)
return yaml;
}

function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateStr: string) {
function buildTemplateVariables(variables: PackagePolicyConfigRecord) {
const yamlValues: { [k: string]: any } = {};
const vars = Object.entries(variables).reduce((acc, [key, recordEntry]) => {
// support variables with . like key.patterns
const keyParts = key.split('.');
const lastKeyPart = keyParts.pop();

if (!lastKeyPart || !isValidKey(lastKeyPart)) {
throw new Error('Invalid key');
throw new PackageInvalidArchiveError(
`Error while compiling agent template: Invalid key ${lastKeyPart}`
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 nice to use typed errors on these cases.

}

let varPart = acc;
for (const keyPart of keyParts) {
if (!isValidKey(keyPart)) {
throw new Error('Invalid key');
throw new PackageInvalidArchiveError(
`Error while compiling agent template: Invalid key ${keyPart}`
);
}
if (!varPart[keyPart]) {
varPart[keyPart] = {};
Expand Down
7 changes: 4 additions & 3 deletions x-pack/plugins/fleet/server/services/epm/archive/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
InstallSource,
PackageAssetReference,
} from '../../../../common/types';
import { PackageInvalidArchiveError, PackageNotFoundError } from '../../../errors';

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

Expand Down Expand Up @@ -70,13 +71,13 @@ export async function archiveEntryToESDocument(opts: {

// validation: filesize? asset type? anything else
if (dataUtf8.length > currentMaxAssetBytes) {
throw new Error(
throw new PackageInvalidArchiveError(
`File at ${path} is larger than maximum allowed size of ${currentMaxAssetBytes}`
);
}

if (dataBase64.length > currentMaxAssetBytes) {
throw new Error(
throw new PackageInvalidArchiveError(
`After base64 encoding file at ${path} is larger than maximum allowed size of ${currentMaxAssetBytes}`
);
}
Expand Down Expand Up @@ -113,7 +114,7 @@ export async function saveArchiveEntries(opts: {
const bulkBody = await Promise.all(
paths.map((path) => {
const buffer = getArchiveEntry(path);
if (!buffer) throw new Error(`Could not find ArchiveEntry at ${path}`);
if (!buffer) throw new PackageNotFoundError(`Could not find ArchiveEntry at ${path}`);
const { name, version } = packageInfo;
return archiveEntryToBulkCreateObject({ path, buffer, name, version, installSource });
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { getAsset, getPathParts } from '../../archive';
import { updateEsAssetReferences } from '../../packages/install';
import { getESAssetMetadata } from '../meta';
import { retryTransientEsErrors } from '../retry';
import { PackageInvalidArchiveError } from '../../../../errors';

export async function installILMPolicy(
packageInfo: InstallablePackage,
Expand Down Expand Up @@ -57,7 +58,7 @@ export async function installILMPolicy(
{ logger }
);
} catch (err) {
throw new Error(err.message);
throw new PackageInvalidArchiveError(`Couldn't install ilm policies: ${err.message}`);
}
})
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from '../../../../constants';
import { getESAssetMetadata } from '../meta';
import { retryTransientEsErrors } from '../retry';
import { PackageESError, PackageInvalidArchiveError } from '../../../../errors';

import { getDefaultProperties, histogram, keyword, scaledFloat } from './mappings';

Expand Down Expand Up @@ -102,7 +103,9 @@ export function getTemplate({
isIndexModeTimeSeries,
});
if (template.template.settings.index.final_pipeline) {
throw new Error(`Error template for ${templateIndexPattern} contains a final_pipeline`);
throw new PackageInvalidArchiveError(
`Error template for ${templateIndexPattern} contains a final_pipeline`
);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm just not sure if these errors should be a 500. I found a few that happen when dealing with ES but I'm not totally sure what the correct response should be.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The template here refers to the template obtained from the package? If this depends only on the content of the package this should probably be a PackageInvalidArchiveError, or some other error indicating that the package is invalid, and produce a 4xx error.

Btw, since elastic/package-spec#587, we are more strict on what can be defined in the index template settings and we don't allow final_pipeline, so a package with final_pipeline in the template settings is indeed invalid.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think you're right, that is indeed the template obtained from the package. I'll change it to PackageInvalidArchiveError

}

const esBaseComponents = getBaseEsComponents(type, !!isIndexModeTimeSeries);
Expand Down Expand Up @@ -427,8 +430,8 @@ function _generateMappings(
matchingType = field.object_type_mapping_type ?? 'object';
break;
default:
throw new Error(
`no dynamic mapping generated for field ${path} of type ${field.object_type}`
throw new PackageInvalidArchiveError(
`No dynamic mapping generated for field ${path} of type ${field.object_type}`
);
}

Expand Down Expand Up @@ -908,7 +911,9 @@ const rolloverDataStream = (dataStreamName: string, esClient: ElasticsearchClien
alias: dataStreamName,
});
} catch (error) {
throw new Error(`cannot rollover data stream [${dataStreamName}] due to error: ${error}`);
throw new PackageESError(
`Cannot rollover data stream [${dataStreamName}] due to error: ${error}`
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👍 this actually looks like a case where we should return a 500 error.

}
};

Expand Down Expand Up @@ -1055,7 +1060,11 @@ const updateExistingDataStream = async ({
{ logger }
);
} catch (err) {
throw new Error(`could not update lifecycle settings for ${dataStreamName}: ${err.message}`);
// Check if this error can happen because of invalid settings;
// We are returning a 500 but in that case it should be a 400 instead
throw new PackageESError(
`Could not update lifecycle settings for ${dataStreamName}: ${err.message}`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I wonder if this can happen because the new settings are invalid, in this case we should produce a 400 error. But I am fine with producing a 500 error by now.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll add a comment to it so we can reconsider it in the future.

);
}
}

Expand All @@ -1078,6 +1087,8 @@ const updateExistingDataStream = async ({
{ logger }
);
} catch (err) {
throw new Error(`could not update index template settings for ${dataStreamName}`);
// Same as above - Check if this error can happen because of invalid settings;
// We are returning a 500 but in that case it should be a 400 instead
throw new PackageESError(`Could not update index template settings for ${dataStreamName}`);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same thing here. If the new index template settings are invalid this should produce a 400 error. But I am fine with leaving this as a 500 error if we don't have a way to discern the cases.

}
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { savedObjectTypes } from '../../packages';
import { indexPatternTypes, getIndexPatternSavedObjects } from '../index_pattern/install';
import { saveKibanaAssetsRefs } from '../../packages/install';
import { deleteKibanaSavedObjectsAssets } from '../../packages/remove';
import { KibanaSOReferenceError } from '../../../../errors';

import { withPackageSpan } from '../../packages/utils';

Expand Down Expand Up @@ -340,7 +341,7 @@ export async function installKibanaSavedObjects({
);

if (otherErrors?.length) {
throw new Error(
throw new KibanaSOReferenceError(
`Encountered ${
otherErrors.length
} errors creating saved objects: ${formatImportErrorsForLog(otherErrors)}`
Expand Down Expand Up @@ -383,7 +384,7 @@ export async function installKibanaSavedObjects({
});

if (resolveErrors?.length) {
throw new Error(
throw new KibanaSOReferenceError(
`Encountered ${
resolveErrors.length
} errors resolving reference errors: ${formatImportErrorsForLog(resolveErrors)}`
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/server/services/epm/package_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import type {
} from '../../types';
import type { FleetAuthzRouteConfig } from '../security/types';
import { checkSuperuser, getAuthzFromRequest, doesNotHaveRequiredFleetAuthz } from '../security';
import { FleetUnauthorizedError } from '../../errors';
import { FleetUnauthorizedError, FleetError } from '../../errors';
import { INSTALL_PACKAGES_AUTHZ, READ_PACKAGE_INFO_AUTHZ } from '../../routes/epm';

import { installTransforms, isTransform } from './elasticsearch/transform/install';
Expand Down Expand Up @@ -208,7 +208,7 @@ class PackageClientImpl implements PackageClient {
const transformPaths = assetPaths.filter(isTransform);

if (transformPaths.length !== assetPaths.length) {
throw new Error('reinstallEsAssets is currently only implemented for transform assets');
throw new FleetError('reinstallEsAssets is currently only implemented for transform assets');
}

if (transformPaths.length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import fs from 'fs/promises';
import path from 'path';

import type { BundledPackage, Installation } from '../../../types';
import { FleetError } from '../../../errors';
import { BundledPackageLocationNotFoundError } from '../../../errors';
import { appContextService } from '../../app_context';
import { splitPkgKey, pkgToPkgKey } from '../registry';

Expand All @@ -19,7 +19,9 @@ export async function getBundledPackages(): Promise<BundledPackage[]> {
const bundledPackageLocation = config?.developer?.bundledPackageLocation;

if (!bundledPackageLocation) {
throw new FleetError('xpack.fleet.developer.bundledPackageLocation is not configured');
throw new BundledPackageLocationNotFoundError(
'xpack.fleet.developer.bundledPackageLocation is not configured'
);
}

// If the bundled package directory is missing, we log a warning during setup,
Expand Down Expand Up @@ -51,7 +53,7 @@ export async function getBundledPackages(): Promise<BundledPackage[]> {
return result;
} catch (err) {
const logger = appContextService.getLogger();
logger.debug(`Unable to read bundled packages from ${bundledPackageLocation}`);
logger.warn(`Unable to read bundled packages from ${bundledPackageLocation}`);

return [];
}
Expand Down
Loading