Aidan Ewen
Aidan Ewen

Reputation: 13328

Fix DNS on a docker-compose selenium grid so the selenium node connects to a docker-compose hostname

I have a selenium grid running under docker-compose on a Jenkins machine. My docker-compose includes a simple web server that serves up a single page application, and a test-runner container that orchestrates tests.

version: "3"
services:
  hub:
    image: selenium/hub
    networks:
      - selenium
    privileged: true
    restart: unless-stopped
    container_name: hub
    ports:
      - "4444:4444"
    environment:
      - SE_OPTS=-browserTimeout 10 -timeout 20
  chrome:
    image: selenium/node-chrome-debug
    networks:
      - selenium
    privileged: true
    restart: unless-stopped
    volumes:
      - /dev/shm:/dev/shm
    depends_on:
      - hub
    environment:
      - HUB_HOST=hub
      - HUB_PORT=4444
      - SE_OPTS=-browserTimeout 10 -timeout 20
    ports:
      - "5900:5900"
  firefox:
    image: selenium/node-firefox-debug
    networks:
      - selenium
    privileged: true
    restart: unless-stopped
    volumes:
      - /dev/shm:/dev/shm
    depends_on:
      - hub
    environment:
      - HUB_HOST=hub
      - HUB_PORT=4444
      - SE_OPTS=-browserTimeout 10 -timeout 20
    ports:
      - "5901:5900"
  runner:
    build:
      context: ./
      dockerfile: ./python.dockerfile
    security_opt:
      - seccomp=unconfined    
    cap_add:
      -  SYS_PTRACE
    command: sleep infinity
    networks:
      - selenium
    volumes:
      - ./:/app
    depends_on:
      - hub
      - app
      - chrome
      - firefox
    environment:
      HUB_CONNECTION_STRING: http://hub:4444/wd/hub
      TEST_DOMAIN: "app"
  app:
    image: nginx:alpine
    networks:
      - selenium
    volumes:
      - ../dist:/usr/share/nginx/html
    ports:
      - "8081:80"
networks:
  selenium:

When my tests run (in the runner container above) I can load the home page as long as I use an ip address -

  def test_home_page_loads(self):
    host = socket.gethostbyname(self.test_domain) // this is the TEST_DOMAIN env var above
    self.driver.get(f"http://{host}")
    header = WebDriverWait(self.driver, 40).until(
      EC.presence_of_element_located((By.ID, 'welcome-message')))
    assert(self.driver.title == "My Page Title")
    assert(header.text == "My Header")

But I can't use the host name app. The following times out -

  def test_home_page_with_hostname(self):
    self.driver.get("http://app/")
    email = WebDriverWait(self.driver, 10).until(
      EC.presence_of_element_located((By.ID, 'email')))

The problem I'm facing is that I can't do all this using IP addresses because the web app is connecting to an external IP and I need to configure the API for CORS requests.

Upvotes: 0

Views: 2452

Answers (1)

Aidan Ewen
Aidan Ewen

Reputation: 13328

I'd assumed the problem was that the chrome container couldn't reach the app container - the issue was that the web server on the app container wasn't serving pages for the hostname I was using. Updating the Nginx conf to include the correct server has fixed the issue.

I can now add the hostname to the access-control-allow-origin settings on the api's that the webpage is using.

I'm attaching a basic working config here for anyone else looking to do something similar.

docker-compose.yml

version: "3"
services:
  hub:
    image: selenium/hub
    networks:
      - selenium
    privileged: true
    restart: unless-stopped
    container_name: hub
    ports:
      - "4444:4444"
    environment:
      - SE_OPTS=-browserTimeout 10 -timeout 20
  chrome:
    image: selenium/node-chrome-debug
    networks:
      - selenium
    privileged: true
    restart: unless-stopped
    volumes:
      - /dev/shm:/dev/shm
    depends_on:
      - hub
    environment:
      - HUB_HOST=hub
      - HUB_PORT=4444
      - SE_OPTS=-browserTimeout 10 -timeout 20
    ports:
      - "5900:5900"
  firefox:
    image: selenium/node-firefox-debug
    networks:
      - selenium
    privileged: true
    restart: unless-stopped
    volumes:
      - /dev/shm:/dev/shm
    depends_on:
      - hub
    environment:
      - HUB_HOST=hub
      - HUB_PORT=4444
      - SE_OPTS=-browserTimeout 10 -timeout 20
    ports:
      - "5901:5900"
  runner:
    build:
      context: ./
      dockerfile: ./python.dockerfile
    security_opt:
      - seccomp=unconfined    
    cap_add:
      -  SYS_PTRACE
    command: sleep infinity
    networks:
      - selenium
    volumes:
      - ./:/app
    depends_on:
      - hub
      - webserver
      - chrome
      - firefox
    environment:
      HUB_CONNECTION_STRING: http://hub:4444/wd/hub
      TEST_DOMAIN: "webserver"
  webserver:
    image: nginx:alpine
    networks:
      - selenium
    volumes:
      - ../dist:/usr/share/nginx/html
      - ./nginx_conf:/etc/nginx/conf.d
    ports:
      - "8081:80"
networks:
  selenium:

default.conf

server {
    listen       80;
    server_name  webserver;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

The 'runner' container is based on the docker image from python:3 and includes pytest. A simple working test looks like -

test.py

from selenium import webdriver
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
import os
import pytest
import socket

#Fixture for Chrome
@pytest.fixture(scope="class")
def chrome_driver_init(request):
  hub_connection_string = os.getenv('HUB_CONNECTION_STRING')
  test_domain = os.getenv('TEST_DOMAIN')
  chrome_driver = webdriver.Remote(
      command_executor=hub_connection_string,
      desired_capabilities={
        'browserName': 'chrome',
        'version': '',
        "chrome.switches": ["disable-web-security"],
        'platform': 'ANY'})
  request.cls.driver = chrome_driver
  request.cls.test_domain = test_domain
  yield
  chrome_driver.close()

@pytest.mark.usefixtures("chrome_driver_init")
class Basic_Chrome_Test:
  driver = None
  test_domain = None
  pass

class Test_Atlas(Basic_Chrome_Test):
  def test_home_page_loads(self):
    self.driver.get(f"http://{self.test_domain}")
    header = WebDriverWait(self.driver, 40).until(
      EC.presence_of_element_located((By.ID, 'welcome-message')))
    assert(self.driver.title == "My Page Title")
    assert(header.text == "My Header")

This can be run with something like docker exec -it $(docker-compose ps -q runner) pytest test.py (exec into the runner container and run the tests using pytest).

This framework can then be added to a Jenkins step -

Jenkinsfile

stage('Run Functional Tests') {
  steps {
    echo 'Running Selenium Grid'
    dir("${env.WORKSPACE}/functional_testing") {
      sh "/usr/local/bin/docker-compose -f ${env.WORKSPACE}/functional_testing/docker-compose.yml -p ${currentBuild.displayName} run runner ./wait-for-webserver.sh pytest tests/atlas_test.py"
    }
  }
}

wait-for-webserver.sh

#!/bin/bash
# wait-for-webserver.sh

set -e

cmd="$@"

while ! curl -sSL "http://hub:4444/wd/hub/status" 2>&1 \
        | jq -r '.value.ready' 2>&1 | grep "true" >/dev/null; do
    echo 'Waiting for the Grid'
    sleep 1
done


while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://webserver)" != "200" ]]; do 
    echo 'Waiting for Webserver'
    sleep 1; 
done

>&2 echo "Grid & Webserver are ready - executing tests"
exec $cmd

Hope this is useful for someone.

Upvotes: 2

Related Questions