@codefresh-io/yaml-validator
Version:
An NPM module/CLI for validating the Codefresh YAML
339 lines (319 loc) • 13.2 kB
JavaScript
'use strict';
const _ = require('lodash');
const BaseSchema = require('./../base-schema');
const { ErrorType, ErrorBuilder } = require('./../error-builder');
const { docBaseUrl, DocumentationLinks, IntegrationLinks, ExternalLinks } = require('./../documentation-links'); // eslint-disable-line
const AWS_REGIONS = [
'us-east-2',
'us-east-1',
'us-west-1',
'us-west-2',
'af-south-1',
'ap-east-1',
'ap-south-1',
'ap-northeast-3',
'ap-northeast-2',
'ap-southeast-1',
'ap-southeast-2',
'ap-northeast-1',
'ca-central-1',
'eu-central-1',
'eu-west-1',
'eu-west-2',
'eu-south-',
'eu-west-3',
'eu-north-1',
'me-south-1',
'sa-east-1',
];
const isWebUri = function (s) {
if (s) {
const patterns = [
/^(?:https?:\/\/)?/, // protocol
/(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9][a-z0-9-]*[a-z0-9]/, // hostname
/(?::\d+)?/, // port
/(?:\/[a-z0-9%@_.~+&:-]*)*/, // path
/(?:\?[a-z0-9%@_.,;&~+:=-]*)?/, // query
/(?:#[a-z0-9_-]*)?$/, // fragment locator
];
const finalPattern = _.reduce(patterns, (acc, value) => acc + value.source, '');
const regex = new RegExp(finalPattern, 'i');
return !!regex.test(s);
}
return false;
};
const validateRegistryContext = function (step,
yaml,
name,
context) {
const errors = [];
const warnings = [];
const errorPath = 'registry_contexts';
const key = 'registry_contexts';
const registryContext = BaseSchema._getFieldFromStep(step, 'registry_context');
const registryContexts = BaseSchema._getFieldFromStep(step, 'registry_contexts');
const stepType = _.get(step, 'type', 'freestyle');
if (registryContexts && _.isArray(registryContexts)) {
const domains = [];
let hasDomainError = false;
registryContexts.forEach((registryCtx) => {
if (!registryCtx || BaseSchema.isRuntimeVariable(registryCtx)) {
return;
}
const registry = _.find(context.registries, { name: registryCtx });
if (!registry) {
errors.push(ErrorBuilder.buildError({
message: `Registry '${registryCtx}' does not exist.`,
name,
yaml,
code: 202,
type: ErrorType.Error,
docsLink: _.get(IntegrationLinks, stepType),
errorPath,
key,
actionItems: 'Please check the spelling or add a new registry in your account settings.',
}));
} else {
if (_.includes(domains, registry.domain) && !hasDomainError) {
hasDomainError = true;
errors.push(ErrorBuilder.buildError({
message: `Registry contexts contains registries with same domain '${registry.domain}'`,
name,
yaml,
code: 207,
type: ErrorType.Error,
docsLink: _.get(DocumentationLinks, stepType),
errorPath,
key,
actionItems: 'Please make sure that there is no more than one registry from the same domain',
}));
}
domains.push(registry.domain);
}
});
}
if (registryContext && !_.isArray(registryContext) && !BaseSchema.isRuntimeVariable(registryContext)
&& !_.some(context.registries, (obj) => { return obj.name === registryContext; })) {
errors.push(ErrorBuilder.buildError({
message: `Registry '${registryContext}' does not exist.`,
name,
yaml,
code: 202,
type: ErrorType.Error,
docsLink: _.get(IntegrationLinks, stepType),
errorPath,
key: 'registry_context',
actionItems: 'Please check the spelling or add a new registry in your account settings.',
}));
}
if (stepType === 'freestyle' && !registryContext && step.role_arn) {
errors.push(ErrorBuilder.buildError({
message: `Cross-account pulling requires specifying a registry integration`,
name,
yaml,
code: 202,
type: ErrorType.Error,
docsLink: _.get(IntegrationLinks, stepType),
errorPath,
key: 'registry_context',
actionItems: 'Please add the registry_context property.',
}));
}
return { errors, warnings };
};
const validate = function (step,
yaml,
name,
context,
{
handleIfNoRegistriesOnAccount, handleIfNoRegistryExcplicitlyDefined, ignoreValidation, handleCFCRRemovalUseCase // eslint-disable-line
}) {
const errorPath = 'registry';
const key = 'registry'; // eslint-disable-line
const { errors, warnings } = validateRegistryContext(step, yaml, name, context);
const registry = BaseSchema._getFieldFromStep(step, 'registry');
if (registry && !_.isString(registry)) {
return { errors, warnings };
}
if (handleCFCRRemovalUseCase && !registry && !step.disable_push && !context.autoPush && !context.disablePush) {
errors.push(ErrorBuilder.buildError({
message: `'registry' is required`,
name,
yaml,
type: ErrorType.Error,
code: 204,
docsLink: _.get(DocumentationLinks, step.type),
errorPath
}));
}
const hasDefaultRegistry = _.find(context.registries, reg => reg.default);
if (handleCFCRRemovalUseCase && context.autoPush && !registry && !hasDefaultRegistry) {
warnings.push(ErrorBuilder.buildError({
message: `The image that will be built will not be pushed`,
name,
yaml,
type: ErrorType.Warning,
code: 205,
docsLink: _.get(DocumentationLinks, step.type),
errorPath
}));
}
if (isWebUri(registry)) {
// Skips validation when registry field contains url.
// Example of this pipeline located at __tests__/test-yamls/yaml-with-registry-url.yml.
return { errors, warnings };
}
if (_.isEmpty(context.registries) && handleIfNoRegistriesOnAccount) {
errors.push(ErrorBuilder.buildError({
message: 'You have not added a registry integration.',
name,
yaml,
type: ErrorType.Error,
code: 200,
docsLink: _.get(IntegrationLinks, step.type),
errorPath,
actionItems: 'Add one in your account settings to continue.',
}));
} else if (registry) {
if (BaseSchema.isRuntimeVariable(registry)) {
if (BaseSchema.isRuntimeVariablesNotContainsStepVariable(context.variables, registry)) {
const variableName = BaseSchema.getVariableNameFromStep(registry);
warnings.push(ErrorBuilder.buildError({
message: `Your registry integration uses a variable '${variableName}' that is not configured and will fail without defining it.`,
name,
yaml,
code: 201,
type: ErrorType.Warning,
docsLink: _.get(IntegrationLinks, 'variables'),
errorPath: 'variables',
key,
}));
}
} else if (!_.some(context.registries, (obj) => { return obj.name === registry; })) {
errors.push(ErrorBuilder.buildError({
message: `Registry '${registry}' does not exist.`,
name,
yaml,
code: 202,
type: ErrorType.Error,
docsLink: _.get(IntegrationLinks, step.type),
errorPath,
key,
actionItems: 'Please check the spelling or add a new registry in your account settings.',
}));
}
} else if (!registry && context.registries.length > 1 && handleIfNoRegistryExcplicitlyDefined && !ignoreValidation) {
const defaultRegistryName = BaseSchema._getDefaultNameFromContext(context.registries, 'name', { default: true });
warnings.push(ErrorBuilder.buildError({
message: `You are using the default registry integration '${defaultRegistryName}'.`,
name,
yaml,
code: 203,
type: ErrorType.Warning,
docsLink: _.get(DocumentationLinks, step.type, docBaseUrl),
errorPath,
actionItems: 'You have additional integrations configured which can be used if defined explicitly.'
}));
}
const provider = _.get(step, 'provider', _.get(step, 'arguments.provider', {}));
if (_.get(provider, 'type', 'cf') === 'gcb') {
if (!_.get(provider, 'arguments.google_app_creds') && !_.some(context.registries, (obj) => { return obj.kind === 'google'; })) {
errors.push(ErrorBuilder.buildError({
message: `provider.arguments.google_app_creds is required`,
name,
yaml,
code: 206,
type: ErrorType.Error,
docsLink: _.get(DocumentationLinks, step.type, docBaseUrl),
errorPath,
key,
actionItems: 'Add google container registry as an integration or provide an explicit credentials key',
}));
}
}
if (step.region) {
if (!AWS_REGIONS.find(currentRegion => currentRegion === step.region) && !BaseSchema.isRuntimeVariable(step.region)) {
errors.push(ErrorBuilder.buildError({
message: `aws region is invalid`,
name,
yaml,
code: 206,
type: ErrorType.Error,
docsLink: _.get(DocumentationLinks, step.type, docBaseUrl),
errorPath,
key,
actionItems: 'Please make sure the specified region is written in the correct format',
}));
} else {
const integrationDefinedProvider = (_.find(context.registries, reg => reg.name === registry) || {}).provider;
if (integrationDefinedProvider !== 'ecr') {
errors.push(ErrorBuilder.buildError({
message: `Unable to specify region with a registry of type: ${integrationDefinedProvider}`,
name,
yaml,
code: 206,
type: ErrorType.Error,
docsLink: _.get(DocumentationLinks, step.type, docBaseUrl),
errorPath,
key,
actionItems: 'Cross-region pushes are currently supported only for ECR',
}));
}
}
}
if (step.role_arn) {
// example for a valid role_arn: arn:aws:iam::559912345678:role/test-role
const splitRoleArn = step.role_arn.split(':');
if (splitRoleArn.length < 4
|| splitRoleArn[0] !== 'arn'
|| splitRoleArn[2] !== 'iam'
|| splitRoleArn[4].length !== 12
|| splitRoleArn[5].substring(0, 'role/'.length) !== 'role/'
) {
errors.push(ErrorBuilder.buildError({
message: `Invalid role_arn`,
name,
yaml,
code: 206,
type: ErrorType.Error,
docsLink: ExternalLinks['reference-identifiers'],
errorPath,
key,
actionItems: 'Please fix the role_arn property',
}));
}
}
if (step.aws_duration_seconds) {
if (!step.role_arn) {
errors.push(ErrorBuilder.buildError({
message: `aws_duration_seconds is only relevant when using role chaining`,
name,
yaml,
code: 206,
type: ErrorType.Error,
docsLink: _.get(DocumentationLinks, step.type, docBaseUrl),
errorPath,
key,
actionItems: 'If you wish to use role chaining, please specify a role_arn to assume',
}));
} else if (step.aws_duration_seconds < 900 || step.aws_duration_seconds > 3600) {
errors.push(ErrorBuilder.buildError({
message: `When using role chaining, the duration of the role session must be between 15 minutes and 1 hour`,
name,
yaml,
code: 206,
type: ErrorType.Error,
docsLink: _.get(DocumentationLinks, step.type, docBaseUrl),
errorPath,
key,
actionItems: 'Please specify a durationSeconds value between 900 and 3600',
}));
}
}
return { errors, warnings };
};
module.exports = {
validate,
validateRegistryContext,
isWebUri,
};