How can I do volume mapping (mounting volumes) in dock:dind in GitLab CI?

Viewed 1911

I am trying to run a Selenium download test in GitLab CI. I use docker:dind in the gitlab-ci.yml file. The test failed because of no files being really downloaded. What should I do?

The test script:

package seleniumTest;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

public class LoginTest_chrome_ci {
    WebDriver driver;
    File folder;

    @BeforeMethod
    public void setUp() throws  MalformedURLException {

        String directory = "/target";
        folder = new File(directory);

        System.setProperty("webdriver.chrome.driver", "chromedriver.exe");
        ChromeOptions options = new ChromeOptions();

        Map<String, Object> prefs = new HashMap<String, Object>();
        prefs.put("profile.default_content_settings.popups", 0);
        prefs.put("download.prompt_for_download", "false");
        prefs.put("download.directory_upgrade", "true");
        prefs.put("download.default_directory", folder.getAbsolutePath());

        options.setExperimentalOption("prefs", prefs);
        DesiredCapabilities cap = new DesiredCapabilities();
        cap.setBrowserName("chrome");
        cap.setCapability(ChromeOptions.CAPABILITY, options);

        driver = new RemoteWebDriver(new URL("http://docker:4444/wd/hub"), cap);
    }

    @AfterMethod
    public void tearDown() {
        driver.quit();

    }

    @Test
    public void downloadFileTest() throws InterruptedException {
        driver.get("http://the-internet.herokuapp.com/download");
        driver.findElement(By.linkText("some-file.txt")).click();

        Thread.sleep(4000);

        File listOffFiles[] = folder.listFiles();

        System.out.println(listOffFiles.length);
        Assert.assertTrue(listOffFiles.length > 0);

        for(File file : listOffFiles) {
            Assert.assertTrue(file.length() > 0);
        }
    }
}

In the docker-compose file, I have done the volume mapping for the Chrome container:

version: "3"
services:
  chrome:
    image: selenium/node-chrome:4.0.0-20211013
    container_name: chrome
    shm_size: 2gb
    depends_on:
      - selenium-hub
    volumes:
      - /target:/home/seluser/Downloads
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SE_NODE_GRID_URL=http://localhost:4444
    ports:
      - "6900:5900"
  selenium-hub:
    image: selenium/hub:4.0.0-20211013
    container_name: selenium-hub
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"

And the gitlab-ci.yml file I use is:

image: adoptopenjdk/openjdk11

stages:
  - gradle-build
  - docker-test

.gradle_template: &gradle_definition
  variables:
    GRADLE_OPTS: "-Dorg.gradle.daemon=false"
  before_script:
    - export GRADLE_USER_HOME=${CI_PROJECT_DIR}/.gradle

gradle-build:
  <<: *gradle_definition
  stage: gradle-build
  script:
    - chmod +x ./gradlew
    - ./gradlew --build-cache assemble
  cache:
    key: "$CI_COMMIT_REF_NAME"
    paths:
      - build
      - .gradle
  artifacts:
    paths:
      - build/libs/*.jar
    expire_in: 1 day
  only:
    - feature/multi-browsers

chrome-test:
  stage: docker-test
  image:
    name: docker/compose:1.29.2
    entrypoint: [ "/bin/sh", "-c" ]
  services:
    - docker:19.03.12-dind
  variables:
    DOCKER_TLS_CERTDIR: ""
    DOCKER_DRIVER: overlay2
    DOCKER_HOST: tcp://docker:2375/
  cache:
    key: "$CI_COMMIT_REF_NAME"
    policy: pull
    paths:
      - build
      - .gradle
  dependencies:
    - gradle-build
  before_script:
    - docker info
    - docker-compose --version
  script:
    - apk add --no-cache docker-compose
    - apk add openjdk11
    - mkdir /target
    - chmod -R 777 /target
    - docker-compose down
    - docker-compose up --build --force-recreate --no-deps -d
    - echo "Hello, chrome-test"
    - chmod +x ./gradlew
    - ./gradlew :test --tests "LoginTest_chrome_ci"

  artifacts:
    when: always
    reports:
      junit: build/test-results/test/**/TEST-*.xml
    paths:
      - build/reports/*
    expire_in: 1 day
  only:
    - feature/multi-browsers

after_script:
  - echo "End CI"

The error I got is:

Gradle suite > Gradle test > seleniumTest.LoginTest_chrome_ci > downloadFileTest FAILED
    java.lang.AssertionError at LoginTest_chrome_ci.java:71
2 Answers

With docker:dind service, volume mapping with docker-compose works more or less as per normal.

I would recommend using a relative path, however, instead of an absolute path in the root. e.g. ./target instead of /target.

Mounting a directory with docker:dind

As a minimal reproducible example of this:

Take the following compose file:

version: "3"
services:
  test:
    image: busybox
    entrypoint: ["/bin/sh", "-c", "echo hello > /opt/data/foo.txt"]
    volumes:
      - ./target:/opt/data

This will create a single container with a mount point at the relative directory ./target and mount it to /opt/data in the container.
When the service container starts, it will simply create a file /opt/data/foo.txt with the text hello.

And the following gitlab CI yaml:

test:
  image: docker/compose
  services:
    - docker:dind
  script:
    - ls # no target directory exists
    - docker-compose up
    - ls target  # target directory was created and foo.txt exists
    - cat ./target/foo.txt  # see the contents of foo.txt as "hello"

Therefore, your error must be the result of something other than the mount point not mounting your directory.

Permissions considerations

File/directory permissions still apply to bind mounts. So, the user in the container needs permission to read/write the mounted directory/files.

The problem you are almost certainly experiencing in this case is that the user in your selenium/node-chrome:4.0.0-20211013 docker container has a UID/GID of 1200/1201. Therefore, the directory that's created in your job by docker-compose for the volume mount won't be writable by the user in the container.

For instance, the same minimal example above -- the job would fail with a "permission denied" error if the image were changed from busybox(which runs as root, same as the job container) to selenium/node-chrome:4.0.0-20211013, which runs with a different user UID/GID.

Therefore, it can be concluded that your issue is that Chrome is unable to write to the ~/Downloads directory and that's why you're not seeing the files in the downloads directory.

How to fix your issue

To remedy this, you should create the mount point ahead of time chown it to the uid/gid of the user in the container.

So two things:

  1. Change the directory to ./target instead of /target (update compose file and your code)
  2. in your GitLab-CI job, pre-create ./target and chown 1200:1201 ./target before starting docker-compose

Something like this will resolve the permissions issue:

before_script:
  - mkdir -p ./target
  - chown 1200:1201 ./target

I fixed a similar issue with a Docker runner and Docker-in-Docker by just changing a couple of values of config.toml (as indicated in this GitLab issue):

  1. Set privileged = true
  2. Add "/builds:/builds" to volumes
Related