UNPKG

7.71 kBPlain TextView Raw
1import { Dockerfile, Instruction } from "dockerfile-ast";
2import { UnresolvedDockerfileVariableHandling } from "../types";
3import {
4 DockerFileAnalysisErrorCode,
5 DockerFileLayers,
6 DockerFilePackages,
7 GetDockerfileBaseImageNameResult,
8} from "./types";
9
10export {
11 getDockerfileBaseImageName,
12 getLayersFromPackages,
13 getPackagesFromDockerfile,
14 instructionDigest,
15 getPackagesFromRunInstructions,
16};
17
18// Naive regex; see tests for cases
19// tslint:disable-next-line:max-line-length
20const installRegex =
21 /(rpm\s+-i|rpm\s+--install|apk\s+((--update|-u|--no-cache)\s+)*add(\s+(--update|-u|--no-cache))*|apt-get\s+((--assume-yes|--yes|-y)\s+)*install(\s+(--assume-yes|--yes|-y))*|apt\s+((--assume-yes|--yes|-y)\s+)*install|yum\s+install|aptitude\s+install)\s+/;
22
23function getPackagesFromDockerfile(dockerfile: Dockerfile): DockerFilePackages {
24 const runInstructions = getRunInstructionsFromDockerfile(dockerfile);
25 return getPackagesFromRunInstructions(runInstructions);
26}
27
28function getRunInstructionsFromDockerfile(dockerfile: Dockerfile) {
29 return dockerfile
30 .getInstructions()
31 .filter(
32 (instruction) => instruction.getInstruction().toUpperCase() === "RUN",
33 )
34 .map((instruction) =>
35 getInstructionExpandVariables(
36 instruction,
37 dockerfile,
38 UnresolvedDockerfileVariableHandling.Continue,
39 ),
40 );
41}
42
43/*
44 * This is fairly ugly because a single RUN could contain multiple install
45 * commands, which in turn may install multiple packages, so we've got a
46 * 3-level nested array (RUN instruction[] -> install[] -> package[])
47 *
48 * We also need to account for the multiple ways to split commands, and
49 * arbitrary whitespace
50 */
51function getPackagesFromRunInstructions(runInstructions: string[]) {
52 return runInstructions.reduce((dockerfilePackages, instruction) => {
53 const cleanedInstruction = cleanInstruction(instruction);
54 const commands = cleanedInstruction.split(/\s?(;|&&)\s?/);
55 const installCommands = commands.filter((command) =>
56 installRegex.test(command),
57 );
58
59 if (installCommands.length) {
60 // Get the packages per install command and flatten them
61 for (const command of installCommands) {
62 const packages = command
63 .replace(installRegex, "")
64 .split(/\s+/)
65 .filter((arg) => arg && !arg.startsWith("-"));
66
67 packages.forEach((pkg) => {
68 // Use package name without version as the key
69 let name = pkg.split("=")[0];
70 if (name.startsWith("$")) {
71 name = name.slice(1);
72 }
73 const installCommand =
74 installCommands
75 .find((cmd) => cmd.indexOf(name) !== -1)
76 ?.replace(/\s+/g, " ")
77 .trim() || "Unknown";
78 dockerfilePackages[name] = {
79 instruction,
80 installCommand,
81 };
82 });
83 }
84 }
85
86 return dockerfilePackages;
87 }, {});
88}
89
90/**
91 * Return the instruction text without any of the image prefixes
92 * @param instruction the full RUN instruction extracted from image
93 */
94function cleanInstruction(instruction: string): string {
95 let cleanedInstruction = instruction;
96 const runDefs = ["RUN ", "/bin/sh ", "-c "];
97 const argsPrefixRegex = /^\|\d .*?=/;
98
99 for (const runDef of runDefs) {
100 if (cleanedInstruction.startsWith(runDef)) {
101 cleanedInstruction = cleanedInstruction.slice(runDef.length);
102 if (runDef === runDefs[0] && argsPrefixRegex.test(cleanedInstruction)) {
103 const match = installRegex.exec(cleanedInstruction);
104 if (match) {
105 cleanedInstruction = cleanedInstruction.slice(match.index);
106 }
107 }
108 }
109 }
110 return cleanedInstruction;
111}
112
113/**
114 * Return the specified text with variables expanded
115 * @param instruction the instruction associated with this string
116 * @param dockerfile Dockerfile to use for expanding the variables
117 * @param unresolvedVariableHandling Strategy for reacting to unresolved vars
118 * @param text a string with variables to expand, if not specified
119 * the instruction text is used
120 */
121function getInstructionExpandVariables(
122 instruction: Instruction,
123 dockerfile: Dockerfile,
124 unresolvedVariableHandling: UnresolvedDockerfileVariableHandling,
125 text?: string,
126): string {
127 let str = text || instruction.toString();
128 const resolvedVariables = {};
129
130 for (const variable of instruction.getVariables()) {
131 const line = variable.getRange().start.line;
132 const name = variable.getName();
133 resolvedVariables[name] = dockerfile.resolveVariable(name, line);
134 }
135 for (const variable of Object.keys(resolvedVariables)) {
136 if (
137 unresolvedVariableHandling ===
138 UnresolvedDockerfileVariableHandling.Abort &&
139 !resolvedVariables[variable]
140 ) {
141 str = "";
142 break;
143 }
144
145 // The $ is a special regexp character that should be escaped with a backslash
146 // Support both notations either with $variable_name or ${variable_name}
147 // The global search "g" flag is used to match and replace all occurrences
148 str = str.replace(
149 RegExp(`\\$\{${variable}\}|\\$${variable}`, "g"),
150 resolvedVariables[variable] || "",
151 );
152 }
153
154 return str;
155}
156
157/**
158 * Return the image name of the last from stage, after resolving all aliases
159 * @param dockerfile Dockerfile to use for retrieving the last stage image name
160 */
161function getDockerfileBaseImageName(
162 dockerfile: Dockerfile,
163): GetDockerfileBaseImageNameResult {
164 const froms = dockerfile.getFROMs();
165 // collect stages names
166 const stagesNames = froms.reduce(
167 (stagesNames, fromInstruction) => {
168 const fromName = fromInstruction.getImage() as string;
169 const args = fromInstruction.getArguments();
170 // the FROM expanded base name
171 const expandedName = getInstructionExpandVariables(
172 fromInstruction,
173 dockerfile,
174 UnresolvedDockerfileVariableHandling.Abort,
175 fromName,
176 );
177
178 const hasUnresolvedVariables =
179 expandedName.split(":").some((name) => !name) ||
180 expandedName.split("@").some((name) => !name);
181
182 if (args.length > 2 && args[1].getValue().toUpperCase() === "AS") {
183 // the AS alias name
184 const aliasName = args[2].getValue();
185 // support nested referral
186 if (!expandedName) {
187 stagesNames.aliases[aliasName] = null;
188 } else {
189 stagesNames.aliases[aliasName] =
190 stagesNames.aliases[expandedName] || expandedName;
191 }
192 }
193
194 const hasUnresolvedAlias =
195 Object.keys(stagesNames.aliases).includes(expandedName) &&
196 !stagesNames.aliases[expandedName];
197
198 if (expandedName === "" || hasUnresolvedVariables || hasUnresolvedAlias) {
199 return {
200 ...stagesNames,
201 last: undefined,
202 };
203 }
204
205 // store the resolved stage name
206 stagesNames.last = stagesNames.aliases[expandedName] || expandedName;
207
208 return stagesNames;
209 },
210 { last: undefined, aliases: {} },
211 );
212
213 if (stagesNames.last) {
214 return {
215 baseImage: stagesNames.last,
216 };
217 }
218
219 if (!froms.length) {
220 return {
221 error: {
222 code: DockerFileAnalysisErrorCode.BASE_IMAGE_NAME_NOT_FOUND,
223 },
224 };
225 }
226
227 return {
228 error: {
229 code: DockerFileAnalysisErrorCode.BASE_IMAGE_NON_RESOLVABLE,
230 },
231 };
232}
233
234function instructionDigest(instruction): string {
235 return Buffer.from(instruction).toString("base64");
236}
237
238function getLayersFromPackages(
239 dockerfilePkgs: DockerFilePackages,
240): DockerFileLayers {
241 return Object.keys(dockerfilePkgs).reduce((res, pkg) => {
242 const { instruction } = dockerfilePkgs[pkg];
243 res[instructionDigest(instruction)] = { instruction };
244 return res;
245 }, {});
246}