UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

350 lines • 15.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.extractVariables = extractVariables; exports.splitImageParts = splitImageParts; exports.getDep = getDep; exports.extractPackageFile = extractPackageFile; const tslib_1 = require("tslib"); const is_1 = tslib_1.__importDefault(require("@sindresorhus/is")); const logger_1 = require("../../../logger"); const regex_1 = require("../../../util/regex"); const docker_1 = require("../../datasource/docker"); const debianVersioning = tslib_1.__importStar(require("../../versioning/debian")); const ubuntuVersioning = tslib_1.__importStar(require("../../versioning/ubuntu")); const variableMarker = '$'; function extractVariables(image) { const variables = {}; const variableRegex = (0, regex_1.regEx)(/(?<fullvariable>\\?\$(?<simplearg>\w+)|\\?\${(?<complexarg>\w+)(?::.+?)?}+)/gi); let match; do { match = variableRegex.exec(image); if (match?.groups?.fullvariable) { variables[match.groups.fullvariable] = match.groups?.simplearg || match.groups?.complexarg; } } while (match); return variables; } function getAutoReplaceTemplate(dep) { let template = dep.replaceString; if (dep.currentValue) { let placeholder = '{{#if newValue}}{{newValue}}{{/if}}'; if (!dep.currentDigest) { placeholder += '{{#if newDigest}}@{{newDigest}}{{/if}}'; } template = template?.replace(dep.currentValue, placeholder); } if (dep.currentDigest) { template = template?.replace(dep.currentDigest, '{{#if newDigest}}{{newDigest}}{{/if}}'); } return template; } function processDepForAutoReplace(dep, lineNumberRanges, lines, linefeed) { const lineNumberRangesToReplace = []; for (const lineNumberRange of lineNumberRanges) { for (const lineNumber of lineNumberRange) { if ((is_1.default.string(dep.currentValue) && lines[lineNumber].includes(dep.currentValue)) || (is_1.default.string(dep.currentDigest) && lines[lineNumber].includes(dep.currentDigest))) { lineNumberRangesToReplace.push(lineNumberRange); } } } lineNumberRangesToReplace.sort((a, b) => { return a[0] - b[0]; }); const minLine = lineNumberRangesToReplace[0]?.[0]; const maxLine = lineNumberRangesToReplace[lineNumberRangesToReplace.length - 1]?.[1]; if (lineNumberRanges.length === 1 || minLine === undefined || maxLine === undefined) { return; } const unfoldedLineNumbers = Array.from({ length: maxLine - minLine + 1 }, (_v, k) => k + minLine); dep.replaceString = unfoldedLineNumbers .map((lineNumber) => lines[lineNumber]) .join(linefeed); if (!dep.currentDigest) { dep.replaceString += linefeed; } dep.autoReplaceStringTemplate = getAutoReplaceTemplate(dep); } function splitImageParts(currentFrom) { let isVariable = false; let cleanedCurrentFrom = currentFrom; // Check if we have a variable in format of "${VARIABLE:-<image>:<defaultVal>@<digest>}" // If so, remove everything except the image, defaultVal and digest. if (cleanedCurrentFrom?.includes(variableMarker)) { const defaultValueRegex = (0, regex_1.regEx)(/^\${.+?:-"?(?<value>.*?)"?}$/); const defaultValueMatch = defaultValueRegex.exec(cleanedCurrentFrom)?.groups; if (defaultValueMatch?.value) { isVariable = true; cleanedCurrentFrom = defaultValueMatch.value; } if (cleanedCurrentFrom?.includes(variableMarker)) { // If cleanedCurrentFrom contains a variable, after cleaning, e.g. "$REGISTRY/alpine", we do not support this. return { skipReason: 'contains-variable', }; } } const [currentDepTag, currentDigest] = cleanedCurrentFrom.split('@'); const depTagSplit = currentDepTag.split(':'); let depName; let currentValue; if (depTagSplit.length === 1 || depTagSplit[depTagSplit.length - 1].includes('/')) { depName = currentDepTag; } else { currentValue = depTagSplit.pop(); depName = depTagSplit.join(':'); } const dep = { depName, packageName: depName, currentValue, currentDigest, }; if (isVariable) { dep.replaceString = cleanedCurrentFrom; if (!dep.currentValue) { delete dep.currentValue; } if (!dep.currentDigest) { delete dep.currentDigest; } } return dep; } const quayRegex = (0, regex_1.regEx)(/^quay\.io(?::[1-9][0-9]{0,4})?/i); function getDep(currentFrom, specifyReplaceString = true, registryAliases) { if (!is_1.default.string(currentFrom) || !is_1.default.nonEmptyStringAndNotWhitespace(currentFrom)) { return { skipReason: 'invalid-value', }; } // Resolve registry aliases first so that we don't need special casing later on: for (const [name, value] of Object.entries(registryAliases ?? {})) { if (currentFrom.startsWith(`${name}/`)) { const depName = currentFrom.substring(name.length + 1); const dep = { ...getDep(`${value}/${depName}`, false), replaceString: currentFrom, }; // retain depName, not sure if condition is necessary if (dep.depName?.startsWith(value)) { dep.packageName = dep.depName; dep.depName = `${name}/${dep.depName.substring(value.length + 1)}`; } if (specifyReplaceString) { dep.autoReplaceStringTemplate = getAutoReplaceTemplate(dep); } return dep; } } const dep = splitImageParts(currentFrom); if (specifyReplaceString) { dep.replaceString ??= currentFrom; dep.autoReplaceStringTemplate = '{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}'; } dep.datasource = docker_1.DockerDatasource.id; // Pretty up special prefixes if (dep.depName) { const specialPrefixes = ['amd64', 'arm64', 'library']; for (const prefix of specialPrefixes) { if (dep.depName.startsWith(`${prefix}/`)) { dep.depName = dep.depName.replace(`${prefix}/`, ''); if (specifyReplaceString) { dep.autoReplaceStringTemplate = '{{packageName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}'; } } } } if (dep.depName === 'ubuntu' || dep.depName?.endsWith('/ubuntu')) { dep.versioning = ubuntuVersioning.id; } if ((dep.depName === 'debian' || dep.depName?.endsWith('/debian')) && debianVersioning.api.isVersion(dep.currentValue)) { dep.versioning = debianVersioning.id; } // Don't display quay.io ports if (dep.depName && quayRegex.test(dep.depName)) { const depName = dep.depName.replace(quayRegex, 'quay.io'); if (depName !== dep.depName) { dep.depName = depName; dep.autoReplaceStringTemplate = '{{packageName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}'; } } return dep; } function extractPackageFile(content, _packageFile, config) { const sanitizedContent = content.replace((0, regex_1.regEx)(/^\uFEFF/), ''); // remove bom marker const deps = []; const stageNames = []; const args = {}; const argsLines = {}; let escapeChar = '\\\\'; let lookForEscapeChar = true; let lookForSyntaxDirective = true; const lineFeed = sanitizedContent.includes('\r\n') ? '\r\n' : '\n'; const lines = sanitizedContent.split(regex_1.newlineRegex); for (let lineNumber = 0; lineNumber < lines.length;) { const lineNumberInstrStart = lineNumber; let instruction = lines[lineNumber]; if (lookForEscapeChar) { const directivesMatch = (0, regex_1.regEx)(/^[ \t]*#[ \t]*(?<directive>syntax|escape)[ \t]*=[ \t]*(?<escapeChar>\S)/i).exec(instruction); if (!directivesMatch) { lookForEscapeChar = false; } else if (directivesMatch.groups?.directive.toLowerCase() === 'escape') { if (directivesMatch.groups?.escapeChar === '`') { escapeChar = '`'; } lookForEscapeChar = false; } } if (lookForSyntaxDirective) { const syntaxRegex = (0, regex_1.regEx)('^#[ \\t]*syntax[ \\t]*=[ \\t]*(?<image>\\S+)', 'im'); const syntaxMatch = instruction.match(syntaxRegex); if (syntaxMatch?.groups?.image) { const syntaxImage = syntaxMatch.groups.image; const lineNumberRanges = [ [lineNumberInstrStart, lineNumber], ]; const dep = getDep(syntaxImage, true, config.registryAliases); dep.depType = 'syntax'; processDepForAutoReplace(dep, lineNumberRanges, lines, lineFeed); logger_1.logger.trace({ depName: dep.depName, currentValue: dep.currentValue, currentDigest: dep.currentDigest, }, 'Dockerfile # syntax'); deps.push(dep); } lookForSyntaxDirective = false; } const lineContinuationRegex = (0, regex_1.regEx)(escapeChar + '[ \\t]*$|^[ \\t]*#', 'm'); let lineLookahead = instruction; while (!lookForEscapeChar && !instruction.trimStart().startsWith('#') && lineContinuationRegex.test(lineLookahead)) { lineLookahead = lines[++lineNumber] || ''; instruction += '\n' + lineLookahead; } const argRegex = (0, regex_1.regEx)('^[ \\t]*ARG(?:' + escapeChar + '[ \\t]*\\r?\\n| |\\t|#.*?\\r?\\n)+(?<name>\\w+)[ =](?<value>\\S*)', 'im'); const argMatch = argRegex.exec(instruction); if (argMatch?.groups?.name) { argsLines[argMatch.groups.name] = [lineNumberInstrStart, lineNumber]; let argMatchValue = argMatch.groups?.value; if (argMatchValue.startsWith('"') && argMatchValue.endsWith('"')) { argMatchValue = argMatchValue.slice(1, -1); } args[argMatch.groups.name] = argMatchValue || ''; } const fromRegex = new RegExp('^[ \\t]*FROM(?:' + escapeChar + '[ \\t]*\\r?\\n| |\\t|#.*?\\r?\\n|--platform=\\S+)+(?<image>\\S+)(?:(?:' + escapeChar + '[ \\t]*\\r?\\n| |\\t|#.*?\\r?\\n)+as[ \\t]+(?<name>\\S+))?', 'im'); // TODO #12875 complex for re2 has too many not supported groups const fromMatch = instruction.match(fromRegex); if (fromMatch?.groups?.image) { let fromImage = fromMatch.groups.image; const lineNumberRanges = [[lineNumberInstrStart, lineNumber]]; if (fromImage.includes(variableMarker)) { const variables = extractVariables(fromImage); for (const [fullVariable, argName] of Object.entries(variables)) { const resolvedArgValue = args[argName]; if (resolvedArgValue || resolvedArgValue === '') { fromImage = fromImage.replace(fullVariable, resolvedArgValue); lineNumberRanges.push(argsLines[argName]); } } } if (fromMatch.groups?.name) { logger_1.logger.debug(`Found a multistage build stage name: ${fromMatch.groups.name}`); stageNames.push(fromMatch.groups.name); } if (fromImage === 'scratch') { logger_1.logger.debug('Skipping scratch'); } else if (fromImage && stageNames.includes(fromImage)) { logger_1.logger.debug(`Skipping alias FROM image:${fromImage}`); } else { const dep = getDep(fromImage, true, config.registryAliases); processDepForAutoReplace(dep, lineNumberRanges, lines, lineFeed); logger_1.logger.trace({ depName: dep.depName, currentValue: dep.currentValue, currentDigest: dep.currentDigest, }, 'Dockerfile FROM'); deps.push(dep); } } const copyFromRegex = new RegExp('^[ \\t]*COPY(?:' + escapeChar + '[ \\t]*\\r?\\n| |\\t|#.*?\\r?\\n|--[a-z]+(?:=[a-zA-Z0-9_.:-]+?)?)+--from=(?<image>\\S+)', 'im'); // TODO #12875 complex for re2 has too many not supported groups const copyFromMatch = instruction.match(copyFromRegex); if (copyFromMatch?.groups?.image) { if (stageNames.includes(copyFromMatch.groups.image)) { logger_1.logger.debug({ image: copyFromMatch.groups.image }, 'Skipping alias COPY --from'); } else if (Number.isNaN(Number(copyFromMatch.groups.image))) { const dep = getDep(copyFromMatch.groups.image, true, config.registryAliases); const lineNumberRanges = [ [lineNumberInstrStart, lineNumber], ]; processDepForAutoReplace(dep, lineNumberRanges, lines, lineFeed); logger_1.logger.debug({ depName: dep.depName, currentValue: dep.currentValue, currentDigest: dep.currentDigest, }, 'Dockerfile COPY --from'); deps.push(dep); } else { logger_1.logger.debug({ image: copyFromMatch.groups.image }, 'Skipping index reference COPY --from'); } } const runMountFromRegex = (0, regex_1.regEx)('^[ \\t]*RUN(?:' + escapeChar + '[ \\t]*\\r?\\n| |\\t|#.*?\\r?\\n|--[a-z]+(?:=[a-zA-Z0-9_.:-]+?)?)+--mount=(?:\\S*=\\S*,)*from=(?<image>[^, ]+)', 'im'); const runMountFromMatch = instruction.match(runMountFromRegex); if (runMountFromMatch?.groups?.image) { if (stageNames.includes(runMountFromMatch.groups.image)) { logger_1.logger.debug({ image: runMountFromMatch.groups.image }, 'Skipping alias RUN --mount=from'); } else { const dep = getDep(runMountFromMatch.groups.image, true, config.registryAliases); const lineNumberRanges = [ [lineNumberInstrStart, lineNumber], ]; processDepForAutoReplace(dep, lineNumberRanges, lines, lineFeed); logger_1.logger.debug({ depName: dep.depName, currentValue: dep.currentValue, currentDigest: dep.currentDigest, }, 'Dockerfile RUN --mount=from'); deps.push(dep); } } lineNumber += 1; } if (!deps.length) { return null; } for (const d of deps) { d.depType ??= 'stage'; } deps[deps.length - 1].depType = 'final'; return { deps }; } //# sourceMappingURL=extract.js.map