@docker-doctor/cli 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +2 -2
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/cli.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +11 -4
- package/dist/index.d.mts +11 -4
- package/dist/index.mjs +1 -1
- package/dist/{src-TK9DRoRo.mjs → src-CvOv-hL3.mjs} +85 -69
- package/dist/src-CvOv-hL3.mjs.map +1 -0
- package/dist/{src-ILbJuJmE.cjs → src-DwuAaQcq.cjs} +89 -67
- package/dist/src-DwuAaQcq.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/src-ILbJuJmE.cjs.map +0 -1
- package/dist/src-TK9DRoRo.mjs.map +0 -1
|
@@ -33,7 +33,7 @@ node_path = __toESM(node_path, 1);
|
|
|
33
33
|
let yaml = require("yaml");
|
|
34
34
|
|
|
35
35
|
//#region package.json
|
|
36
|
-
var version = "0.4.
|
|
36
|
+
var version = "0.4.4";
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
39
|
//#region ../core/src/project-info/discover.ts
|
|
@@ -136,7 +136,6 @@ const processHeredocLine = (state, trimmed) => {
|
|
|
136
136
|
};
|
|
137
137
|
const processInstructionLine = (state, trimmed, lineNum) => {
|
|
138
138
|
let lineContent = trimmed;
|
|
139
|
-
if (lineContent.startsWith("#")) return;
|
|
140
139
|
const hasContinuation = lineContent.endsWith("\\");
|
|
141
140
|
if (hasContinuation) lineContent = lineContent.slice(0, -1).trim();
|
|
142
141
|
if (state.currentInstruction) state.currentArgs += (state.currentArgs ? " " : "") + lineContent;
|
|
@@ -159,7 +158,8 @@ const parseDockerfile = (content) => {
|
|
|
159
158
|
const trimmed = rawLine.trim();
|
|
160
159
|
const lineNum = i + 1;
|
|
161
160
|
const insideHeredoc = state.heredocQueue.length > 0;
|
|
162
|
-
if (!
|
|
161
|
+
if (!insideHeredoc && trimmed.startsWith("#")) continue;
|
|
162
|
+
if (!state.currentInstruction && !insideHeredoc && trimmed === "") continue;
|
|
163
163
|
state.rawAccumulator.push(rawLine);
|
|
164
164
|
if (insideHeredoc) processHeredocLine(state, trimmed);
|
|
165
165
|
else processInstructionLine(state, trimmed, lineNum);
|
|
@@ -202,6 +202,40 @@ const parseCompose = (content, filepath) => {
|
|
|
202
202
|
});
|
|
203
203
|
}
|
|
204
204
|
};
|
|
205
|
+
/**
|
|
206
|
+
* Builds a {@link ComposeLocator} over the same source text a compose object
|
|
207
|
+
* was parsed from, so rules can attach line numbers to their diagnostics.
|
|
208
|
+
*
|
|
209
|
+
* Keys pulled in via YAML merge keys (`<<: *anchor`) have no concrete node
|
|
210
|
+
* at the merge site, so paths through them resolve to `undefined` — callers
|
|
211
|
+
* fall back to an unnumbered diagnostic, which matches the old behavior.
|
|
212
|
+
*/
|
|
213
|
+
const createComposeLocator = (content) => {
|
|
214
|
+
const lineCounter = new yaml.LineCounter();
|
|
215
|
+
const doc = (0, yaml.parseDocument)(content, {
|
|
216
|
+
lineCounter,
|
|
217
|
+
merge: true
|
|
218
|
+
});
|
|
219
|
+
return (path) => {
|
|
220
|
+
let node = doc.contents;
|
|
221
|
+
let offset;
|
|
222
|
+
for (const segment of path) {
|
|
223
|
+
if ((0, yaml.isAlias)(node)) node = node.resolve(doc);
|
|
224
|
+
if ((0, yaml.isMap)(node)) {
|
|
225
|
+
const pair = node.items.find((item) => (0, yaml.isScalar)(item.key) && String(item.key.value) === String(segment));
|
|
226
|
+
if (!pair || !(0, yaml.isScalar)(pair.key)) return;
|
|
227
|
+
offset = pair.key.range?.[0];
|
|
228
|
+
node = pair.value;
|
|
229
|
+
} else if ((0, yaml.isSeq)(node) && typeof segment === "number") {
|
|
230
|
+
const item = node.items[segment];
|
|
231
|
+
if (item === void 0 || item === null) return;
|
|
232
|
+
offset = item.range?.[0];
|
|
233
|
+
node = item;
|
|
234
|
+
} else return;
|
|
235
|
+
}
|
|
236
|
+
return offset === void 0 ? void 0 : lineCounter.linePos(offset).line;
|
|
237
|
+
};
|
|
238
|
+
};
|
|
205
239
|
|
|
206
240
|
//#endregion
|
|
207
241
|
//#region ../core/src/parsers/exec-form.ts
|
|
@@ -277,8 +311,8 @@ const collectStageAliases = (instructions) => {
|
|
|
277
311
|
};
|
|
278
312
|
|
|
279
313
|
//#endregion
|
|
280
|
-
//#region ../core/src/rules/
|
|
281
|
-
const createDiagnostic
|
|
314
|
+
//#region ../core/src/rules/create-diagnostic.ts
|
|
315
|
+
const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
|
|
282
316
|
file,
|
|
283
317
|
help,
|
|
284
318
|
line,
|
|
@@ -286,12 +320,15 @@ const createDiagnostic$4 = (file, ruleKey, severity, message, help, line) => ({
|
|
|
286
320
|
rule: ruleKey,
|
|
287
321
|
severity
|
|
288
322
|
});
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region ../core/src/rules/best-practices.ts
|
|
289
326
|
const requireHealthcheck = {
|
|
290
327
|
category: "Best Practices",
|
|
291
328
|
check(instructions, file) {
|
|
292
329
|
const hasHealthcheck = instructions.some((inst) => inst.instruction === "HEALTHCHECK");
|
|
293
330
|
const hasExposedPortsOrEntry = instructions.some((inst) => inst.instruction === "EXPOSE" || inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT");
|
|
294
|
-
if (!hasHealthcheck && hasExposedPortsOrEntry) return [createDiagnostic
|
|
331
|
+
if (!hasHealthcheck && hasExposedPortsOrEntry) return [createDiagnostic(file, this.key, this.defaultSeverity, "No HEALTHCHECK instruction found. Containers running services should expose healthchecks to enable auto-healing.", this.help, 1)];
|
|
295
332
|
return [];
|
|
296
333
|
},
|
|
297
334
|
defaultSeverity: "info",
|
|
@@ -308,7 +345,7 @@ const preferCopyOverAdd = {
|
|
|
308
345
|
if (!src) continue;
|
|
309
346
|
const isRemote = src.startsWith("http://") || src.startsWith("https://");
|
|
310
347
|
const isArchive = src.endsWith(".tar") || src.endsWith(".tar.gz") || src.endsWith(".tgz") || src.endsWith(".zip");
|
|
311
|
-
if (!isRemote && !isArchive) diagnostics.push(createDiagnostic
|
|
348
|
+
if (!isRemote && !isArchive) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `ADD instruction used for regular files: '${inst.args}'. COPY is simpler and less prone to magic side effects.`, this.help, inst.line));
|
|
312
349
|
}
|
|
313
350
|
return diagnostics;
|
|
314
351
|
},
|
|
@@ -321,7 +358,7 @@ const useExecForm = {
|
|
|
321
358
|
category: "Best Practices",
|
|
322
359
|
check(instructions, file) {
|
|
323
360
|
const diagnostics = [];
|
|
324
|
-
for (const inst of instructions) if ((inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT") && parseExecForm(inst.args) === null) diagnostics.push(createDiagnostic
|
|
361
|
+
for (const inst of instructions) if ((inst.instruction === "CMD" || inst.instruction === "ENTRYPOINT") && parseExecForm(inst.args) === null) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `${inst.instruction} instruction uses shell form instead of exec form. In shell form, the command runs under '/bin/sh -c', which does not pass signals to child processes.`, this.help, inst.line));
|
|
325
362
|
return diagnostics;
|
|
326
363
|
},
|
|
327
364
|
defaultSeverity: "warning",
|
|
@@ -332,7 +369,7 @@ const useExecForm = {
|
|
|
332
369
|
const requireLabels = {
|
|
333
370
|
category: "Best Practices",
|
|
334
371
|
check(instructions, file) {
|
|
335
|
-
if (!instructions.some((inst) => inst.instruction === "LABEL")) return [createDiagnostic
|
|
372
|
+
if (!instructions.some((inst) => inst.instruction === "LABEL")) return [createDiagnostic(file, this.key, this.defaultSeverity, "No LABEL metadata was found in this Dockerfile. Adding labels helps identify build information, maintainers, and descriptions.", this.help, 1)];
|
|
336
373
|
return [];
|
|
337
374
|
},
|
|
338
375
|
defaultSeverity: "info",
|
|
@@ -347,8 +384,8 @@ const combineAptUpdateInstall = {
|
|
|
347
384
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
348
385
|
const hasUpdate = inst.args.includes("apt-get update");
|
|
349
386
|
const hasInstall = inst.args.includes("apt-get install");
|
|
350
|
-
if (hasUpdate && !hasInstall) diagnostics.push(createDiagnostic
|
|
351
|
-
else if (hasInstall && !hasUpdate) diagnostics.push(createDiagnostic
|
|
387
|
+
if (hasUpdate && !hasInstall) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get update used without apt-get install in the same instruction. This can cause caching issues and build failures.", this.help, inst.line));
|
|
388
|
+
else if (hasInstall && !hasUpdate) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN apt-get install used without apt-get update in the same instruction. Always combine them to ensure up-to-date package installation.", this.help, inst.line));
|
|
352
389
|
}
|
|
353
390
|
return diagnostics;
|
|
354
391
|
},
|
|
@@ -388,7 +425,7 @@ const usePipefail = {
|
|
|
388
425
|
const { args } = inst;
|
|
389
426
|
if (!HAS_PIPE_RE.test(args)) continue;
|
|
390
427
|
const execArgv = parseExecForm(args);
|
|
391
|
-
if (!(execArgv === null ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args) : PIPEFAIL_SETTING_RE.test(execArgv.join(" ")))) diagnostics.push(createDiagnostic
|
|
428
|
+
if (!(execArgv === null ? shellHasPipefail || PIPEFAIL_SETTING_RE.test(args) : PIPEFAIL_SETTING_RE.test(execArgv.join(" ")))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN instruction uses a pipe (|) but does not configure 'pipefail'. If a command in the pipe fails, the step may still succeed silently.", this.help, inst.line));
|
|
392
429
|
}
|
|
393
430
|
return diagnostics;
|
|
394
431
|
},
|
|
@@ -403,7 +440,7 @@ const absoluteWorkdir = {
|
|
|
403
440
|
const diagnostics = [];
|
|
404
441
|
for (const inst of instructions) if (inst.instruction === "WORKDIR") {
|
|
405
442
|
const path = inst.args.trim();
|
|
406
|
-
if (!/^(?:\/|\\|\$|[a-zA-Z]:)/u.test(path)) diagnostics.push(createDiagnostic
|
|
443
|
+
if (!/^(?:\/|\\|\$|[a-zA-Z]:)/u.test(path)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `WORKDIR specifies a relative path '${path}'. For clarity and reliability, always use absolute paths.`, this.help, inst.line));
|
|
407
444
|
}
|
|
408
445
|
return diagnostics;
|
|
409
446
|
},
|
|
@@ -416,7 +453,7 @@ const avoidRunCd = {
|
|
|
416
453
|
category: "Best Practices",
|
|
417
454
|
check(instructions, file) {
|
|
418
455
|
const diagnostics = [];
|
|
419
|
-
for (const inst of instructions) if (inst.instruction === "RUN" && /\bcd\b/u.test(inst.args)) diagnostics.push(createDiagnostic
|
|
456
|
+
for (const inst of instructions) if (inst.instruction === "RUN" && /\bcd\b/u.test(inst.args)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Avoid using 'cd' in RUN instructions. Use WORKDIR instead to change the working directory stably across layers.", this.help, inst.line));
|
|
420
457
|
return diagnostics;
|
|
421
458
|
},
|
|
422
459
|
defaultSeverity: "info",
|
|
@@ -436,7 +473,7 @@ const sortMultilineArgs = {
|
|
|
436
473
|
const packages = raw.split(/\r?\n/u).slice(1).map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("&&") && !line.startsWith("-") && !line.includes("rm -rf")).map((line) => line.endsWith("\\") ? line.slice(0, -1).trim() : line).filter(Boolean);
|
|
437
474
|
if (packages.length > 1) {
|
|
438
475
|
const sorted = packages.toSorted((a, b) => a.localeCompare(b));
|
|
439
|
-
if (!packages.every((val, idx) => val === sorted[idx])) diagnostics.push(createDiagnostic
|
|
476
|
+
if (!packages.every((val, idx) => val === sorted[idx])) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "Multi-line package arguments are not sorted alphanumerically. Keeping them sorted makes maintenance easier and prevents duplicates.", this.help, inst.line));
|
|
440
477
|
}
|
|
441
478
|
}
|
|
442
479
|
}
|
|
@@ -451,7 +488,7 @@ const useraddNoLogInit = {
|
|
|
451
488
|
category: "Best Practices",
|
|
452
489
|
check(instructions, file) {
|
|
453
490
|
const diagnostics = [];
|
|
454
|
-
for (const inst of instructions) if (inst.instruction === "RUN" && /\buseradd\b/u.test(inst.args) && !inst.args.includes("--no-log-init")) diagnostics.push(createDiagnostic
|
|
491
|
+
for (const inst of instructions) if (inst.instruction === "RUN" && /\buseradd\b/u.test(inst.args) && !inst.args.includes("--no-log-init")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, "RUN instruction runs 'useradd' without '--no-log-init'. This can cause excessive disk space usage / exhaustion under Go's sparse tar archive bug when large UIDs are used.", this.help, inst.line));
|
|
455
492
|
return diagnostics;
|
|
456
493
|
},
|
|
457
494
|
defaultSeverity: "warning",
|
|
@@ -474,17 +511,10 @@ const bestPracticesRules = [
|
|
|
474
511
|
|
|
475
512
|
//#endregion
|
|
476
513
|
//#region ../core/src/rules/compose.ts
|
|
477
|
-
const createDiagnostic$3 = (file, ruleKey, severity, message, help) => ({
|
|
478
|
-
file,
|
|
479
|
-
help,
|
|
480
|
-
message,
|
|
481
|
-
rule: ruleKey,
|
|
482
|
-
severity
|
|
483
|
-
});
|
|
484
514
|
const noVersionKey = {
|
|
485
515
|
category: "Compose",
|
|
486
|
-
check(composeContent, file) {
|
|
487
|
-
if (composeContent && typeof composeContent === "object" && "version" in composeContent) return [createDiagnostic
|
|
516
|
+
check(composeContent, file, context) {
|
|
517
|
+
if (composeContent && typeof composeContent === "object" && "version" in composeContent) return [createDiagnostic(file, this.key, this.defaultSeverity, "The 'version' property is deprecated. Remove it to use standard Compose spec behavior.", this.help, context?.locate?.(["version"]))];
|
|
488
518
|
return [];
|
|
489
519
|
},
|
|
490
520
|
defaultSeverity: "warning",
|
|
@@ -494,14 +524,14 @@ const noVersionKey = {
|
|
|
494
524
|
};
|
|
495
525
|
const requireResourceLimits = {
|
|
496
526
|
category: "Compose",
|
|
497
|
-
check(composeContent, file) {
|
|
527
|
+
check(composeContent, file, context) {
|
|
498
528
|
const diagnostics = [];
|
|
499
529
|
if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
|
|
500
530
|
const { services } = composeContent;
|
|
501
531
|
if (services && typeof services === "object") {
|
|
502
532
|
for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
|
|
503
533
|
const limits = (config.deploy?.resources)?.limits;
|
|
504
|
-
if (!limits || !limits.cpus && !limits.memory) diagnostics.push(createDiagnostic
|
|
534
|
+
if (!limits || !limits.cpus && !limits.memory) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' does not have CPU or memory limits defined. A resource leak in this service could crash the host.`, this.help, context?.locate?.(["services", name])));
|
|
505
535
|
}
|
|
506
536
|
}
|
|
507
537
|
}
|
|
@@ -514,7 +544,7 @@ const requireResourceLimits = {
|
|
|
514
544
|
};
|
|
515
545
|
const requireRestartPolicy = {
|
|
516
546
|
category: "Compose",
|
|
517
|
-
check(composeContent, file) {
|
|
547
|
+
check(composeContent, file, context) {
|
|
518
548
|
const diagnostics = [];
|
|
519
549
|
if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
|
|
520
550
|
const { services } = composeContent;
|
|
@@ -522,7 +552,7 @@ const requireRestartPolicy = {
|
|
|
522
552
|
for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
|
|
523
553
|
const hasRestart = "restart" in config;
|
|
524
554
|
const hasDeployRestart = config.deploy?.restart_policy !== void 0;
|
|
525
|
-
if (!hasRestart && !hasDeployRestart) diagnostics.push(createDiagnostic
|
|
555
|
+
if (!hasRestart && !hasDeployRestart) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' has no restart policy configured. It will not restart if it crashes or if the host reboots.`, this.help, context?.locate?.(["services", name])));
|
|
526
556
|
}
|
|
527
557
|
}
|
|
528
558
|
}
|
|
@@ -535,14 +565,18 @@ const requireRestartPolicy = {
|
|
|
535
565
|
};
|
|
536
566
|
const useDependsOnCondition = {
|
|
537
567
|
category: "Compose",
|
|
538
|
-
check(composeContent, file) {
|
|
568
|
+
check(composeContent, file, context) {
|
|
539
569
|
const diagnostics = [];
|
|
540
570
|
if (composeContent && typeof composeContent === "object" && "services" in composeContent) {
|
|
541
571
|
const { services } = composeContent;
|
|
542
572
|
if (services && typeof services === "object") {
|
|
543
573
|
for (const [name, config] of Object.entries(services)) if (config && typeof config === "object") {
|
|
544
574
|
const dependsOn = config.depends_on;
|
|
545
|
-
if (dependsOn && Array.isArray(dependsOn)) diagnostics.push(createDiagnostic
|
|
575
|
+
if (dependsOn && Array.isArray(dependsOn)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Service '${name}' uses shorthand depends_on list. This only checks if containers are started, not if they are ready/healthy.`, this.help, context?.locate?.([
|
|
576
|
+
"services",
|
|
577
|
+
name,
|
|
578
|
+
"depends_on"
|
|
579
|
+
]) ?? context?.locate?.(["services", name])));
|
|
546
580
|
}
|
|
547
581
|
}
|
|
548
582
|
}
|
|
@@ -562,14 +596,6 @@ const composeRules = [
|
|
|
562
596
|
|
|
563
597
|
//#endregion
|
|
564
598
|
//#region ../core/src/rules/image-size.ts
|
|
565
|
-
const createDiagnostic$2 = (file, ruleKey, severity, message, help, line) => ({
|
|
566
|
-
file,
|
|
567
|
-
help,
|
|
568
|
-
line,
|
|
569
|
-
message,
|
|
570
|
-
rule: ruleKey,
|
|
571
|
-
severity
|
|
572
|
-
});
|
|
573
599
|
const preferSlimBase = {
|
|
574
600
|
category: "Image Size",
|
|
575
601
|
check(instructions, file) {
|
|
@@ -583,7 +609,7 @@ const preferSlimBase = {
|
|
|
583
609
|
if (ref.digest) continue;
|
|
584
610
|
if (!ref.tag) continue;
|
|
585
611
|
const haystack = `${ref.name} ${ref.tag}`.toLowerCase();
|
|
586
|
-
if (!(haystack.includes("alpine") || haystack.includes("slim") || haystack.includes("distroless") || haystack.includes("busybox"))) diagnostics.push(createDiagnostic
|
|
612
|
+
if (!(haystack.includes("alpine") || haystack.includes("slim") || haystack.includes("distroless") || haystack.includes("busybox"))) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Base image '${imagePart}' may be a full-OS distribution. Consider using a slim or alpine alternative.`, this.help, inst.line));
|
|
587
613
|
}
|
|
588
614
|
return diagnostics;
|
|
589
615
|
},
|
|
@@ -592,14 +618,20 @@ const preferSlimBase = {
|
|
|
592
618
|
key: "docker-doctor/prefer-slim-base",
|
|
593
619
|
message: "Prefer slim, alpine, or distroless base images"
|
|
594
620
|
};
|
|
621
|
+
const BUILDKIT_MOUNT_FLAG_RE = /--mount=(?<spec>\S+)/gu;
|
|
622
|
+
const CACHE_TARGET_KEY_RE = /^(?:target|dst|destination)=/u;
|
|
623
|
+
const cacheMountTargets = (args) => [...args.matchAll(BUILDKIT_MOUNT_FLAG_RE)].map((match) => (match.groups?.spec ?? "").split(",")).filter((options) => options.includes("type=cache")).flatMap((options) => options.filter((option) => CACHE_TARGET_KEY_RE.test(option)).map((option) => option.slice(option.indexOf("=") + 1)));
|
|
624
|
+
const APT_CACHE_DIRS = ["/var/lib/apt", "/var/cache/apt"];
|
|
625
|
+
const APK_CACHE_DIRS = ["/var/cache/apk", "/etc/apk/cache"];
|
|
626
|
+
const hasCacheMountFor = (args, cacheDirs) => cacheMountTargets(args).some((target) => cacheDirs.some((dir) => target === dir || target.startsWith(`${dir}/`)));
|
|
595
627
|
const cleanPackageCache = {
|
|
596
628
|
category: "Image Size",
|
|
597
629
|
check(instructions, file) {
|
|
598
630
|
const diagnostics = [];
|
|
599
631
|
for (const inst of instructions) if (inst.instruction === "RUN") {
|
|
600
632
|
const { args } = inst;
|
|
601
|
-
if (args.includes("apt-get install") && !args.includes("rm -rf /var/lib/apt/lists")) diagnostics.push(createDiagnostic
|
|
602
|
-
if (args.includes("apk add") && !args.includes("--no-cache") && !args.includes("rm -rf /var/cache/apk")) diagnostics.push(createDiagnostic
|
|
633
|
+
if (args.includes("apt-get install") && !args.includes("rm -rf /var/lib/apt/lists") && !hasCacheMountFor(args, APT_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apt-get install' without removing package lists afterwards. This keeps metadata caches inside the image layer.`, this.help, inst.line));
|
|
634
|
+
if (args.includes("apk add") && !args.includes("--no-cache") && !args.includes("rm -rf /var/cache/apk") && !hasCacheMountFor(args, APK_CACHE_DIRS)) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running 'apk add' without '--no-cache' or cleaning the apk cache. This increases layer size.`, this.help, inst.line));
|
|
603
635
|
}
|
|
604
636
|
return diagnostics;
|
|
605
637
|
},
|
|
@@ -636,7 +668,7 @@ const avoidDevDependencies = {
|
|
|
636
668
|
for (const inst of stage.runs) {
|
|
637
669
|
if (!installsDevDependencies(inst.args)) continue;
|
|
638
670
|
const where = stageIndex === finalIndex ? "in the final stage" : `in stage '${stage.name}', whose layers the final stage inherits,`;
|
|
639
|
-
diagnostics.push(createDiagnostic
|
|
671
|
+
diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package install '${inst.args}' ${where} without omitting devDependencies.`, this.help, inst.line));
|
|
640
672
|
}
|
|
641
673
|
}
|
|
642
674
|
return diagnostics;
|
|
@@ -654,19 +686,11 @@ const imageSizeRules = [
|
|
|
654
686
|
|
|
655
687
|
//#endregion
|
|
656
688
|
//#region ../core/src/rules/performance.ts
|
|
657
|
-
const createDiagnostic$1 = (file, ruleKey, severity, message, help, line) => ({
|
|
658
|
-
file,
|
|
659
|
-
help,
|
|
660
|
-
line,
|
|
661
|
-
message,
|
|
662
|
-
rule: ruleKey,
|
|
663
|
-
severity
|
|
664
|
-
});
|
|
665
689
|
const useMultiStage = {
|
|
666
690
|
category: "Performance",
|
|
667
691
|
check(instructions, file) {
|
|
668
692
|
if (instructions.filter((inst) => inst.instruction === "FROM").length === 1) {
|
|
669
|
-
if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || inst.args.includes("make")))) return [createDiagnostic
|
|
693
|
+
if (instructions.some((inst) => inst.instruction === "RUN" && (inst.args.includes("npm run build") || inst.args.includes("yarn build") || inst.args.includes("bun run build") || inst.args.includes("cargo build") || inst.args.includes("make")))) return [createDiagnostic(file, this.key, this.defaultSeverity, "Only one build stage (FROM) was detected, but build instructions were found. Multi-stage builds can significantly reduce final image size.", this.help, instructions.find((inst) => inst.instruction === "FROM")?.line || 1)];
|
|
670
694
|
}
|
|
671
695
|
return [];
|
|
672
696
|
},
|
|
@@ -690,7 +714,7 @@ const orderLayers = {
|
|
|
690
714
|
}
|
|
691
715
|
if (inst.instruction === "RUN" && copyAllLine !== -1) {
|
|
692
716
|
const args = inst.args.toLowerCase();
|
|
693
|
-
if (args.includes("npm install") || args.includes("npm ci") || args.includes("yarn install") || args.includes("bun install") || args.includes("pip install") || args.includes("cargo fetch")) diagnostics.push(createDiagnostic
|
|
717
|
+
if (args.includes("npm install") || args.includes("npm ci") || args.includes("yarn install") || args.includes("bun install") || args.includes("pip install") || args.includes("cargo fetch")) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Running package installation command '${inst.args}' after copying application files (at line ${copyAllLine}). This invalidates the cache on any code changes.`, this.help, inst.line));
|
|
694
718
|
}
|
|
695
719
|
}
|
|
696
720
|
return diagnostics;
|
|
@@ -710,10 +734,10 @@ const minimizeLayers = {
|
|
|
710
734
|
if (consecutiveRunCount === 0) firstRunLine = inst.line;
|
|
711
735
|
consecutiveRunCount += 1;
|
|
712
736
|
} else {
|
|
713
|
-
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic
|
|
737
|
+
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
|
|
714
738
|
consecutiveRunCount = 0;
|
|
715
739
|
}
|
|
716
|
-
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic
|
|
740
|
+
if (consecutiveRunCount > 2) diagnostics.push(createDiagnostic(file, this.key, this.defaultSeverity, `Found ${consecutiveRunCount} consecutive RUN instructions starting at line ${firstRunLine}. Consider combining them into a single RUN layer.`, this.help, firstRunLine));
|
|
717
741
|
return diagnostics;
|
|
718
742
|
},
|
|
719
743
|
defaultSeverity: "info",
|
|
@@ -738,7 +762,7 @@ const useDockerignore = {
|
|
|
738
762
|
return src === "." || src === "./" || src === "*";
|
|
739
763
|
}
|
|
740
764
|
return false;
|
|
741
|
-
}) && context?.projectFiles && !hasDockerignoreFor(file, context.projectFiles)) return [createDiagnostic
|
|
765
|
+
}) && context?.projectFiles && !hasDockerignoreFor(file, context.projectFiles)) return [createDiagnostic(file, this.key, this.defaultSeverity, "Using COPY/ADD with a wildcard or directory, but no .dockerignore file was found next to the Dockerfile or at the project root. This can copy local build folders and secrets.", this.help, 1)];
|
|
742
766
|
return [];
|
|
743
767
|
},
|
|
744
768
|
defaultSeverity: "warning",
|
|
@@ -755,14 +779,6 @@ const performanceRules = [
|
|
|
755
779
|
|
|
756
780
|
//#endregion
|
|
757
781
|
//#region ../core/src/rules/security.ts
|
|
758
|
-
const createDiagnostic = (file, ruleKey, severity, message, help, line) => ({
|
|
759
|
-
file,
|
|
760
|
-
help,
|
|
761
|
-
line,
|
|
762
|
-
message,
|
|
763
|
-
rule: ruleKey,
|
|
764
|
-
severity
|
|
765
|
-
});
|
|
766
782
|
const isRootUser = (value) => {
|
|
767
783
|
const [user] = value.split(":");
|
|
768
784
|
return user === "root" || user === "0";
|
|
@@ -909,12 +925,12 @@ const runDockerfileRules = (instructions, file, projectFiles, rulesConfig, categ
|
|
|
909
925
|
|
|
910
926
|
//#endregion
|
|
911
927
|
//#region ../core/src/runners/compose-runner.ts
|
|
912
|
-
const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig) => {
|
|
928
|
+
const runComposeRules = (composeContent, file, rulesConfig, categoriesConfig, locate) => {
|
|
913
929
|
const diagnostics = [];
|
|
914
930
|
for (const rule of allComposeRules) {
|
|
915
931
|
const severity = resolveSeverity(rule, rulesConfig, categoriesConfig);
|
|
916
932
|
if (severity === "off") continue;
|
|
917
|
-
const ruleDiagnostics = rule.check(composeContent, file);
|
|
933
|
+
const ruleDiagnostics = rule.check(composeContent, file, { locate });
|
|
918
934
|
if (severity !== rule.defaultSeverity) for (const diag of ruleDiagnostics) diag.severity = severity;
|
|
919
935
|
diagnostics.push(...ruleDiagnostics);
|
|
920
936
|
}
|
|
@@ -1163,6 +1179,12 @@ Object.defineProperty(exports, 'calculateScore', {
|
|
|
1163
1179
|
return calculateScore;
|
|
1164
1180
|
}
|
|
1165
1181
|
});
|
|
1182
|
+
Object.defineProperty(exports, 'createComposeLocator', {
|
|
1183
|
+
enumerable: true,
|
|
1184
|
+
get: function () {
|
|
1185
|
+
return createComposeLocator;
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1166
1188
|
Object.defineProperty(exports, 'discoverProject', {
|
|
1167
1189
|
enumerable: true,
|
|
1168
1190
|
get: function () {
|
|
@@ -1217,4 +1239,4 @@ Object.defineProperty(exports, 'version', {
|
|
|
1217
1239
|
return version;
|
|
1218
1240
|
}
|
|
1219
1241
|
});
|
|
1220
|
-
//# sourceMappingURL=src-
|
|
1242
|
+
//# sourceMappingURL=src-DwuAaQcq.cjs.map
|