From 52a8da89fa2d4db6c1ae6208f65af1b15d61b7ff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maximilian=20Ga=C3=9F?= <mxey@mxey.net>
Date: Sat, 11 Aug 2018 11:27:18 +0200
Subject: [PATCH] feat(docker): Add support for COPY --from lines (#2368)

COPY --from= can specify external images. Add support to renovate them.
---
 lib/manager/dockerfile/extract.js             | 27 +++++++++++-
 lib/manager/dockerfile/update.js              | 10 +++--
 .../__snapshots__/extract.spec.js.snap        | 43 +++++++++++++++++++
 .../__snapshots__/update.spec.js.snap         |  6 +++
 test/manager/dockerfile/extract.spec.js       | 15 +++++++
 test/manager/dockerfile/update.spec.js        | 14 ++++++
 6 files changed, 110 insertions(+), 5 deletions(-)

diff --git a/lib/manager/dockerfile/extract.js b/lib/manager/dockerfile/extract.js
index 7dcd7b91a1..fb6a226ec9 100644
--- a/lib/manager/dockerfile/extract.js
+++ b/lib/manager/dockerfile/extract.js
@@ -68,8 +68,8 @@ function extractDependencies(content) {
   const stageNames = [];
   let lineNumber = 0;
   for (const fromLine of content.split('\n')) {
-    const match = fromLine.match(/^FROM /i);
-    if (match) {
+    const fromMatch = fromLine.match(/^FROM /i);
+    if (fromMatch) {
       logger.debug({ lineNumber, fromLine }, 'FROM line');
       const [fromPrefix, currentFrom, ...fromRest] = fromLine.match(/\S+/g);
       if (fromRest.length === 2 && fromRest[0].toLowerCase() === 'as') {
@@ -97,6 +97,29 @@ function extractDependencies(content) {
         deps.push(dep);
       }
     }
+
+    const copyFromMatch = fromLine.match(/^(COPY --from=)([^\s]+)\s+(.*)$/i);
+    if (copyFromMatch) {
+      const [fromPrefix, currentFrom, fromSuffix] = copyFromMatch.slice(1);
+      logger.debug({ lineNumber, fromLine }, 'COPY --from line');
+      if (stageNames.includes(currentFrom)) {
+        logger.debug({ currentFrom }, 'Skipping alias COPY --from');
+      } else {
+        const dep = getDep(currentFrom);
+        logger.info(
+          {
+            depName: dep.depName,
+            currentTag: dep.currentTag,
+            currentDigest: dep.currentDigest,
+          },
+          'Dockerfile COPY --from'
+        );
+        dep.lineNumber = lineNumber;
+        dep.fromPrefix = fromPrefix;
+        dep.fromSuffix = fromSuffix;
+        deps.push(dep);
+      }
+    }
     lineNumber += 1;
   }
   if (!deps.length) {
diff --git a/lib/manager/dockerfile/update.js b/lib/manager/dockerfile/update.js
index 70dcad73ec..7bdd89866a 100644
--- a/lib/manager/dockerfile/update.js
+++ b/lib/manager/dockerfile/update.js
@@ -21,17 +21,21 @@ function getNewFrom(upgrade) {
 
 function updateDependency(fileContent, upgrade) {
   try {
-    const { lineNumber, fromPrefix, fromSuffix } = upgrade;
+    const { lineNumber, fromSuffix } = upgrade;
+    let { fromPrefix } = upgrade;
     const newFrom = getNewFrom(upgrade);
     logger.debug(`docker.updateDependency(): ${newFrom}`);
     const lines = fileContent.split('\n');
     const lineToChange = lines[lineNumber];
-    const imageLine = new RegExp(/^FROM /i);
+    const imageLine = new RegExp(/^(FROM |COPY --from=)/i);
     if (!lineToChange.match(imageLine)) {
       logger.debug('No image line found');
       return null;
     }
-    const newLine = `${fromPrefix} ${newFrom} ${fromSuffix}`.trim();
+    if (!fromPrefix.endsWith('=')) {
+      fromPrefix += ' ';
+    }
+    const newLine = `${fromPrefix}${newFrom} ${fromSuffix}`.trim();
     if (newLine === lineToChange) {
       logger.debug('No changes necessary');
       return fileContent;
diff --git a/test/manager/dockerfile/__snapshots__/extract.spec.js.snap b/test/manager/dockerfile/__snapshots__/extract.spec.js.snap
index e4c22b5fa5..da952dea32 100644
--- a/test/manager/dockerfile/__snapshots__/extract.spec.js.snap
+++ b/test/manager/dockerfile/__snapshots__/extract.spec.js.snap
@@ -76,6 +76,27 @@ Array [
 ]
 `;
 
+exports[`lib/manager/dockerfile/extract extractDependencies() handles COPY --from 1`] = `
+Array [
+  Object {
+    "currentDepTag": "k8s-skaffold/skaffold:v0.11.0",
+    "currentDepTagDigest": "k8s-skaffold/skaffold:v0.11.0",
+    "currentDigest": undefined,
+    "currentFrom": "gcr.io/k8s-skaffold/skaffold:v0.11.0",
+    "currentTag": "v0.11.0",
+    "currentValue": "v0.11.0",
+    "depName": "k8s-skaffold/skaffold",
+    "dockerRegistry": "gcr.io",
+    "fromPrefix": "COPY --from=",
+    "fromSuffix": "/usr/bin/skaffold /usr/bin/skaffold",
+    "lineNumber": 1,
+    "purl": "pkg:docker/k8s-skaffold/skaffold?registry=gcr.io",
+    "tagSuffix": undefined,
+    "versionScheme": "docker",
+  },
+]
+`;
+
 exports[`lib/manager/dockerfile/extract extractDependencies() handles abnoral spacing 1`] = `
 Array [
   Object {
@@ -362,6 +383,28 @@ Array [
 ]
 `;
 
+exports[`lib/manager/dockerfile/extract extractDependencies() skips named multistage COPY --from tags 1`] = `
+Array [
+  Object {
+    "commitMessageTopic": "Node.js",
+    "currentDepTag": "node:6.12.3",
+    "currentDepTagDigest": "node:6.12.3",
+    "currentDigest": undefined,
+    "currentFrom": "node:6.12.3",
+    "currentTag": "6.12.3",
+    "currentValue": "6.12.3",
+    "depName": "node",
+    "dockerRegistry": undefined,
+    "fromPrefix": "FROM",
+    "fromSuffix": "as frontend",
+    "lineNumber": 0,
+    "purl": "pkg:docker/node",
+    "tagSuffix": undefined,
+    "versionScheme": "docker",
+  },
+]
+`;
+
 exports[`lib/manager/dockerfile/extract extractDependencies() skips named multistage FROM tags 1`] = `
 Array [
   Object {
diff --git a/test/manager/dockerfile/__snapshots__/update.spec.js.snap b/test/manager/dockerfile/__snapshots__/update.spec.js.snap
index 8e9dd8c5b3..ef595744b5 100644
--- a/test/manager/dockerfile/__snapshots__/update.spec.js.snap
+++ b/test/manager/dockerfile/__snapshots__/update.spec.js.snap
@@ -14,6 +14,12 @@ RUN something
 "
 `;
 
+exports[`manager/dockerfile/update updateDependency replaces COPY --from 1`] = `
+"FROM scratch
+COPY --from=gcr.io/k8s-skaffold/skaffold:v0.12.0 /usr/bin/skaffold /usr/bin/skaffold
+"
+`;
+
 exports[`manager/dockerfile/update updateDependency replaces existing value 1`] = `
 "# comment FROM node:8
 FROM node:8-alpine@sha256:abcdefghijklmnop
diff --git a/test/manager/dockerfile/extract.spec.js b/test/manager/dockerfile/extract.spec.js
index 48340cf147..6d929adc91 100644
--- a/test/manager/dockerfile/extract.spec.js
+++ b/test/manager/dockerfile/extract.spec.js
@@ -120,6 +120,21 @@ describe('lib/manager/dockerfile/extract', () => {
       expect(res).toMatchSnapshot();
       expect(res).toHaveLength(1);
     });
+    it('handles COPY --from', () => {
+      const res = extractDependencies(
+        'FROM scratch\nCOPY --from=gcr.io/k8s-skaffold/skaffold:v0.11.0 /usr/bin/skaffold /usr/bin/skaffold\n',
+        config
+      ).deps;
+      expect(res).toMatchSnapshot();
+    });
+    it('skips named multistage COPY --from tags', () => {
+      const res = extractDependencies(
+        'FROM node:6.12.3 as frontend\n\n# comment\nENV foo=bar\nCOPY --from=frontend /usr/bin/node /usr/bin/node\n',
+        config
+      ).deps;
+      expect(res).toMatchSnapshot();
+      expect(res).toHaveLength(1);
+    });
     it('extracts images on adjacent lines', () => {
       const res = extractDependencies(d1, config).deps;
       expect(res).toMatchSnapshot();
diff --git a/test/manager/dockerfile/update.spec.js b/test/manager/dockerfile/update.spec.js
index b24c7654c3..3a1c6b881f 100644
--- a/test/manager/dockerfile/update.spec.js
+++ b/test/manager/dockerfile/update.spec.js
@@ -108,5 +108,19 @@ describe('manager/dockerfile/update', () => {
       expect(res).toMatchSnapshot();
       expect(res.includes('as stage-1')).toBe(true);
     });
+    it('replaces COPY --from', () => {
+      const fileContent =
+        'FROM scratch\nCOPY --from=gcr.io/k8s-skaffold/skaffold:v0.11.0 /usr/bin/skaffold /usr/bin/skaffold\n';
+      const upgrade = {
+        lineNumber: 1,
+        depName: 'k8s-skaffold/skaffold',
+        newValue: 'v0.12.0',
+        fromPrefix: 'COPY --from=',
+        fromSuffix: '/usr/bin/skaffold /usr/bin/skaffold',
+        dockerRegistry: 'gcr.io',
+      };
+      const res = dockerfile.updateDependency(fileContent, upgrade);
+      expect(res).toMatchSnapshot();
+    });
   });
 });
-- 
GitLab