From ec43556b2940c1da489908936d0fb7dfd74cb50a Mon Sep 17 00:00:00 2001
From: Wolfgang Faust <wolfgang42@users.noreply.github.com>
Date: Fri, 26 Jun 2020 22:39:44 -0700
Subject: [PATCH] fix(docker-compose): support docker-compose.yml version 1
 (#6596)

---
 .../__fixtures__/docker-compose.1.yml         | 171 ++++++++----------
 .../__fixtures__/docker-compose.3.yml         | 104 +++++++++++
 .../__snapshots__/extract.spec.ts.snap        |  70 ++++++-
 lib/manager/docker-compose/extract.spec.ts    |  19 +-
 lib/manager/docker-compose/extract.ts         |  18 +-
 5 files changed, 277 insertions(+), 105 deletions(-)
 create mode 100644 lib/manager/docker-compose/__fixtures__/docker-compose.3.yml

diff --git a/lib/manager/docker-compose/__fixtures__/docker-compose.1.yml b/lib/manager/docker-compose/__fixtures__/docker-compose.1.yml
index 602f488633..a2ddd4ca6e 100644
--- a/lib/manager/docker-compose/__fixtures__/docker-compose.1.yml
+++ b/lib/manager/docker-compose/__fixtures__/docker-compose.1.yml
@@ -1,104 +1,83 @@
-version: "3"
-services:
+redis:
+  image: quay.io/something/redis:alpine
+  ports:
+    - "6379"
+  deploy:
+    replicas: 2
+    update_config:
+      parallelism: 2
+      delay: 10s
+    restart_policy:
+      condition: on-failure
 
-  redis:
-    image: quay.io/something/redis:alpine
-    ports:
-      - "6379"
-    networks:
-      - frontend
-    deploy:
-      replicas: 2
-      update_config:
-        parallelism: 2
-        delay: 10s
-      restart_policy:
-        condition: on-failure
+worker:
+  image: "node:10.0.0"
 
-  worker:
-    image: "node:10.0.0"
+db:
+  image: "postgres:9.4.0"
+  volumes:
+    - db-data:/var/lib/postgresql/data
+  deploy:
+    placement:
+      constraints: [node.role == manager]
 
-  db:
-    image: "postgres:9.4.0"
-    volumes:
-      - db-data:/var/lib/postgresql/data
-    networks:
-      - backend
-    deploy:
-      placement:
-        constraints: [node.role == manager]
+vote:
+  image: dockersamples/examplevotingapp_vote:before
+  ports:
+    - 5000:80
+  depends_on:
+    - redis
+  deploy:
+    replicas: 2
+    update_config:
+      parallelism: 2
+    restart_policy:
+      condition: on-failure
 
-  vote:
-    image: dockersamples/examplevotingapp_vote:before
-    ports:
-      - 5000:80
-    networks:
-      - frontend
-    depends_on:
-      - redis
-    deploy:
-      replicas: 2
-      update_config:
-        parallelism: 2
-      restart_policy:
-        condition: on-failure
+result:
+  image: 'dockersamples/examplevotingapp_result:before'
+  ports:
+    - 5001:80
+  depends_on:
+    - db
+  deploy:
+    replicas: 1
+    update_config:
+      parallelism: 2
+      delay: 10s
+    restart_policy:
+      condition: on-failure
 
-  result:
-    image: 'dockersamples/examplevotingapp_result:before'
-    ports:
-      - 5001:80
-    networks:
-      - backend
-    depends_on:
-      - db
-    deploy:
-      replicas: 1
-      update_config:
-        parallelism: 2
-        delay: 10s
-      restart_policy:
-        condition: on-failure
+votingworker:
+  image: dockersamples/examplevotingapp_worker
+  deploy:
+    mode: replicated
+    replicas: 1
+    labels: [APP=VOTING]
+    restart_policy:
+      condition: on-failure
+      delay: 10s
+      max_attempts: 3
+      window: 120s
+    placement:
+      constraints: [node.role == manager]
 
-  votingworker:
-    image: dockersamples/examplevotingapp_worker
-    networks:
-      - frontend
-      - backend
-    deploy:
-      mode: replicated
-      replicas: 1
-      labels: [APP=VOTING]
-      restart_policy:
-        condition: on-failure
-        delay: 10s
-        max_attempts: 3
-        window: 120s
-      placement:
-        constraints: [node.role == manager]
+visualizer:
+  image: dockersamples/visualizer:stable
+  ports:
+    - "8080:8080"
+  stop_grace_period: 1m30s
+  volumes:
+    - "/var/run/docker.sock:/var/run/docker.sock"
+  deploy:
+    placement:
+      constraints: [node.role == manager]
 
-  visualizer:
-    image: dockersamples/visualizer:stable
-    ports:
-      - "8080:8080"
-    stop_grace_period: 1m30s
-    volumes:
-      - "/var/run/docker.sock:/var/run/docker.sock"
-    deploy:
-      placement:
-        constraints: [node.role == manager]
+edplugins:
+  image: ${IMAGE:-synkodevelopers/edplugins}:${TAG:-latest}
 
-  edplugins:
-    image: ${IMAGE:-synkodevelopers/edplugins}:${TAG:-latest}
-
-  debugapp:
-    image: app-local-debug
-    build:
-      context: .
-      dockerfile: Dockerfile.local
-
-networks:
-  frontend:
-  backend:
-
-volumes:
-  db-data:
+debugapp:
+  image: app-local-debug
+  build:
+    context: .
+    dockerfile: Dockerfile.local
diff --git a/lib/manager/docker-compose/__fixtures__/docker-compose.3.yml b/lib/manager/docker-compose/__fixtures__/docker-compose.3.yml
new file mode 100644
index 0000000000..602f488633
--- /dev/null
+++ b/lib/manager/docker-compose/__fixtures__/docker-compose.3.yml
@@ -0,0 +1,104 @@
+version: "3"
+services:
+
+  redis:
+    image: quay.io/something/redis:alpine
+    ports:
+      - "6379"
+    networks:
+      - frontend
+    deploy:
+      replicas: 2
+      update_config:
+        parallelism: 2
+        delay: 10s
+      restart_policy:
+        condition: on-failure
+
+  worker:
+    image: "node:10.0.0"
+
+  db:
+    image: "postgres:9.4.0"
+    volumes:
+      - db-data:/var/lib/postgresql/data
+    networks:
+      - backend
+    deploy:
+      placement:
+        constraints: [node.role == manager]
+
+  vote:
+    image: dockersamples/examplevotingapp_vote:before
+    ports:
+      - 5000:80
+    networks:
+      - frontend
+    depends_on:
+      - redis
+    deploy:
+      replicas: 2
+      update_config:
+        parallelism: 2
+      restart_policy:
+        condition: on-failure
+
+  result:
+    image: 'dockersamples/examplevotingapp_result:before'
+    ports:
+      - 5001:80
+    networks:
+      - backend
+    depends_on:
+      - db
+    deploy:
+      replicas: 1
+      update_config:
+        parallelism: 2
+        delay: 10s
+      restart_policy:
+        condition: on-failure
+
+  votingworker:
+    image: dockersamples/examplevotingapp_worker
+    networks:
+      - frontend
+      - backend
+    deploy:
+      mode: replicated
+      replicas: 1
+      labels: [APP=VOTING]
+      restart_policy:
+        condition: on-failure
+        delay: 10s
+        max_attempts: 3
+        window: 120s
+      placement:
+        constraints: [node.role == manager]
+
+  visualizer:
+    image: dockersamples/visualizer:stable
+    ports:
+      - "8080:8080"
+    stop_grace_period: 1m30s
+    volumes:
+      - "/var/run/docker.sock:/var/run/docker.sock"
+    deploy:
+      placement:
+        constraints: [node.role == manager]
+
+  edplugins:
+    image: ${IMAGE:-synkodevelopers/edplugins}:${TAG:-latest}
+
+  debugapp:
+    image: app-local-debug
+    build:
+      context: .
+      dockerfile: Dockerfile.local
+
+networks:
+  frontend:
+  backend:
+
+volumes:
+  db-data:
diff --git a/lib/manager/docker-compose/__snapshots__/extract.spec.ts.snap b/lib/manager/docker-compose/__snapshots__/extract.spec.ts.snap
index f310aa0348..c632b01318 100644
--- a/lib/manager/docker-compose/__snapshots__/extract.spec.ts.snap
+++ b/lib/manager/docker-compose/__snapshots__/extract.spec.ts.snap
@@ -1,6 +1,74 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`lib/manager/docker-compose/extract extractPackageFile() extracts multiple image lines 1`] = `
+exports[`lib/manager/docker-compose/extract extractPackageFile() extracts multiple image lines for version 1 1`] = `
+Array [
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "currentDigest": undefined,
+    "currentValue": "alpine",
+    "datasource": "docker",
+    "depName": "quay.io/something/redis",
+    "replaceString": "quay.io/something/redis:alpine",
+  },
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "commitMessageTopic": "Node.js",
+    "currentDigest": undefined,
+    "currentValue": "10.0.0",
+    "datasource": "docker",
+    "depName": "node",
+    "replaceString": "node:10.0.0",
+  },
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "currentDigest": undefined,
+    "currentValue": "9.4.0",
+    "datasource": "docker",
+    "depName": "postgres",
+    "replaceString": "postgres:9.4.0",
+  },
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "currentDigest": undefined,
+    "currentValue": "before",
+    "datasource": "docker",
+    "depName": "dockersamples/examplevotingapp_vote",
+    "replaceString": "dockersamples/examplevotingapp_vote:before",
+  },
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "currentDigest": undefined,
+    "currentValue": "before",
+    "datasource": "docker",
+    "depName": "dockersamples/examplevotingapp_result",
+    "replaceString": "dockersamples/examplevotingapp_result:before",
+  },
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "currentDigest": undefined,
+    "currentValue": undefined,
+    "datasource": "docker",
+    "depName": "dockersamples/examplevotingapp_worker",
+    "replaceString": "dockersamples/examplevotingapp_worker",
+  },
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "currentDigest": undefined,
+    "currentValue": "stable",
+    "datasource": "docker",
+    "depName": "dockersamples/visualizer",
+    "replaceString": "dockersamples/visualizer:stable",
+  },
+  Object {
+    "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
+    "datasource": "docker",
+    "replaceString": "\${IMAGE:-synkodevelopers/edplugins}:\${TAG:-latest}",
+    "skipReason": "contains-variable",
+  },
+]
+`;
+
+exports[`lib/manager/docker-compose/extract extractPackageFile() extracts multiple image lines for version 3 1`] = `
 Array [
   Object {
     "autoReplaceStringTemplate": "{{depName}}{{#if newValue}}:{{newValue}}{{/if}}{{#if newDigest}}@{{newDigest}}{{/if}}",
diff --git a/lib/manager/docker-compose/extract.spec.ts b/lib/manager/docker-compose/extract.spec.ts
index 19727a4e3d..e0f972bf12 100644
--- a/lib/manager/docker-compose/extract.spec.ts
+++ b/lib/manager/docker-compose/extract.spec.ts
@@ -1,21 +1,34 @@
 import { readFileSync } from 'fs';
 import { extractPackageFile } from './extract';
 
-const yamlFile = readFileSync(
+const yamlFile1 = readFileSync(
   'lib/manager/docker-compose/__fixtures__/docker-compose.1.yml',
   'utf8'
 );
 
+const yamlFile3 = readFileSync(
+  'lib/manager/docker-compose/__fixtures__/docker-compose.3.yml',
+  'utf8'
+);
+
 describe('lib/manager/docker-compose/extract', () => {
   describe('extractPackageFile()', () => {
     it('returns null for empty', () => {
+      expect(extractPackageFile('')).toBeNull();
+    });
+    it('returns null for non-object YAML', () => {
       expect(extractPackageFile('nothing here')).toBeNull();
     });
     it('returns null for malformed YAML', () => {
       expect(extractPackageFile('nothing here\n:::::::')).toBeNull();
     });
-    it('extracts multiple image lines', () => {
-      const res = extractPackageFile(yamlFile);
+    it('extracts multiple image lines for version 1', () => {
+      const res = extractPackageFile(yamlFile1);
+      expect(res.deps).toMatchSnapshot();
+      expect(res.deps).toHaveLength(8);
+    });
+    it('extracts multiple image lines for version 3', () => {
+      const res = extractPackageFile(yamlFile3);
       expect(res.deps).toMatchSnapshot();
       expect(res.deps).toHaveLength(8);
     });
diff --git a/lib/manager/docker-compose/extract.ts b/lib/manager/docker-compose/extract.ts
index 24733098f3..d4ded82ded 100644
--- a/lib/manager/docker-compose/extract.ts
+++ b/lib/manager/docker-compose/extract.ts
@@ -47,7 +47,6 @@ export function extractPackageFile(
   let config: DockerComposeConfig;
   try {
     config = safeLoad(content, { json: true });
-    // istanbul ignore if
     if (!config) {
       logger.debug(
         { fileName },
@@ -55,6 +54,13 @@ export function extractPackageFile(
       );
       return null;
     }
+    if (typeof config !== 'object') {
+      logger.debug(
+        { fileName, type: typeof config },
+        'Unexpected type for Docker Compose content'
+      );
+      return null;
+    }
   } catch (err) {
     logger.debug({ err }, 'err');
     logger.debug({ fileName }, 'Parsing Docker Compose config YAML');
@@ -63,9 +69,14 @@ export function extractPackageFile(
   try {
     const lineMapper = new LineMapper(content, /^\s*image:/);
 
+    const services =
+      'version' in config
+        ? config.services // docker-compose version 2+
+        : config; // docker-compose version 1 (services at top level)
+
     // Image name/tags for services are only eligible for update if they don't
     // use variables and if the image is not built locally
-    const deps = Object.values(config.services || {})
+    const deps = Object.values(services || {})
       .filter((service) => service && service.image && !service.build)
       .map((service) => {
         const dep = getDep(service.image);
@@ -79,9 +90,6 @@ export function extractPackageFile(
       .filter(Boolean);
 
     logger.trace({ deps }, 'Docker Compose image');
-    if (!deps.length) {
-      return null;
-    }
     return { deps };
   } catch (err) /* istanbul ignore next */ {
     logger.warn(
-- 
GitLab