UNPKG

renovate

Version:

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

238 lines (237 loc) • 11.1 kB
import { newlineRegex, regEx } from "../../../util/regex.js"; import { logger } from "../../../logger/index.js"; import api, { id } from "../../versioning/debian/index.js"; import { id as id$1 } from "../../versioning/ubuntu/index.js"; import { DockerDatasource } from "../../datasource/docker/index.js"; import { isNonEmptyStringAndNotWhitespace, isString } from "@sindresorhus/is"; //#region lib/modules/manager/dockerfile/extract.ts const variableMarker = "$"; function extractVariables(image) { const variables = {}; const variableRegex = 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 (isString(dep.currentValue) && lines[lineNumber].includes(dep.currentValue) || isString(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 === void 0 || maxLine === void 0) return; dep.replaceString = Array.from({ length: maxLine - minLine + 1 }, (_v, k) => k + minLine).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; if (cleanedCurrentFrom?.includes(variableMarker)) { const defaultValueMatch = regEx(/^\${.+?:-"?(?<value>.*?)"?}$/).exec(cleanedCurrentFrom)?.groups; if (defaultValueMatch?.value) { isVariable = true; cleanedCurrentFrom = defaultValueMatch.value; } if (cleanedCurrentFrom?.includes(variableMarker)) 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 = regEx(/^quay\.io(?::[1-9][0-9]{0,4})?/i); function getDep(currentFrom, specifyReplaceString = true, registryAliases) { if (!isString(currentFrom) || !isNonEmptyStringAndNotWhitespace(currentFrom)) return { skipReason: "invalid-value" }; for (const [name, value] of Object.entries(registryAliases ?? {})) if (currentFrom.startsWith(`${name}/`)) { const dep = getDep(`${value}/${currentFrom.substring(name.length + 1)}`, false); if (dep.depName?.startsWith(value)) { dep.packageName = dep.depName; dep.depName = `${name}/${dep.depName.substring(value.length + 1)}`; } if (specifyReplaceString) { dep.replaceString = currentFrom; 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 = DockerDatasource.id; if (dep.depName) { for (const prefix of [ "amd64", "arm64", "library" ]) 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 = id$1; if ((dep.depName === "debian" || dep.depName?.endsWith("/debian")) && api.isVersion(dep.currentValue)) dep.versioning = id; 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(regEx(/^\uFEFF/), ""); 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(newlineRegex); for (let lineNumber = 0; lineNumber < lines.length;) { const lineNumberInstrStart = lineNumber; let instruction = lines[lineNumber]; if (lookForEscapeChar) { const directivesMatch = 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 = 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.trace({ depName: dep.depName, currentValue: dep.currentValue, currentDigest: dep.currentDigest }, "Dockerfile # syntax"); deps.push(dep); } lookForSyntaxDirective = false; } const lineContinuationRegex = regEx(`${escapeChar}[ \\t]*$|^[ \\t]*#`, "m"); let lineLookahead = instruction; while (!lookForEscapeChar && !instruction.trimStart().startsWith("#") && lineContinuationRegex.test(lineLookahead)) { lineLookahead = lines[++lineNumber] || ""; instruction += `\n${lineLookahead}`; } const argMatch = regEx(`^[ \\t]*ARG(?:${escapeChar}[ \\t]*\\r?\\n| |\\t|#.*?\\r?\\n)+(?<name>\\w+)[ =](?<value>\\S*)`, "im").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"); 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.replaceAll(fullVariable, resolvedArgValue); lineNumberRanges.push(argsLines[argName]); } } } if (fromMatch.groups?.name) { logger.debug(`Found a multistage build stage name: ${fromMatch.groups.name}`); stageNames.push(fromMatch.groups.name); } if (fromImage === "scratch") logger.debug("Skipping scratch"); else if (fromImage && stageNames.includes(fromImage)) logger.debug(`Skipping alias FROM image:${fromImage}`); else { const dep = getDep(fromImage, true, config.registryAliases); processDepForAutoReplace(dep, lineNumberRanges, lines, lineFeed); 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"); const copyFromMatch = instruction.match(copyFromRegex); if (copyFromMatch?.groups?.image) if (stageNames.includes(copyFromMatch.groups.image)) 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); processDepForAutoReplace(dep, [[lineNumberInstrStart, lineNumber]], lines, lineFeed); logger.debug({ depName: dep.depName, currentValue: dep.currentValue, currentDigest: dep.currentDigest }, "Dockerfile COPY --from"); deps.push(dep); } else logger.debug({ image: copyFromMatch.groups.image }, "Skipping index reference COPY --from"); const runMountFromRegex = 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.debug({ image: runMountFromMatch.groups.image }, "Skipping alias RUN --mount=from"); else { const dep = getDep(runMountFromMatch.groups.image, true, config.registryAliases); processDepForAutoReplace(dep, [[lineNumberInstrStart, lineNumber]], lines, lineFeed); 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 }; } //#endregion export { extractPackageFile, getDep }; //# sourceMappingURL=extract.js.map