Skip to content

Commit

Permalink
add docker pull policy
Browse files Browse the repository at this point in the history
Add pull policy inspired from `docker run --pull=policy` to the
DockerDriver. This adds the possibility to build a docker image locally,
and use it in labgrid without having to have to push it to a registry.

Signed-off-by: Nikolaj Rahbek <[email protected]>
  • Loading branch information
b2vn committed Dec 9, 2024
1 parent 71bfcd2 commit 72647bc
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 2 deletions.
11 changes: 11 additions & 0 deletions doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3084,12 +3084,23 @@ Implements:
DockerDriver:
image_uri: 'rastasheep/ubuntu-sshd:16.04'
pull: 'always'
container_name: 'ubuntu-lg-example'
host_config: {'network_mode': 'bridge'}
network_services: [{'port': 22, 'username': 'root', 'password': 'root'}]
Arguments:
- image_uri (str): identifier of the docker image to use (may have a tag suffix)
- pull (str): pull policy, supports "always", "missing", "never". Default is
"always"

- always: Always pull the image and throw an error if the pull fails.
- missing: Pull the image only when the image is not in the local
containers storage. Throw an error if no image is found and the pull
fails.
- never: Never pull the image but use the one from the local containers
storage. Throw a `docker.errors.ImageNotFound` if no image is found.

- command (str): optional, command to run in the container (depends on image)
- volumes (list): optional, list to configure volumes mounted inside the container
- container_name (str): name of the container
Expand Down
45 changes: 44 additions & 1 deletion labgrid/driver/dockerdriver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""
Class for connecting to a docker daemon running on the host machine.
"""
from enum import Enum

import attr

from labgrid.factory import target_factory
Expand All @@ -9,6 +11,33 @@
from labgrid.protocol.powerprotocol import PowerProtocol


class PullPolicy(Enum):
"""Pull policy for the `DockerDriver`.
Modelled after `podman run --pull` / `docker run --pull`.
* always: Always pull the image and throw an error if the pull fails.
* missing: Pull the image only when the image is not in the local
containers storage. Throw an error if no image is found and the pull
fails.
* never: Never pull the image but use the one from the local containers
storage. Throw an error if no image is found.
* newer: **Note** not supported by the driver, and therefore not
implemented.
"""
Always = 'always'
Missing = 'missing'
Never = 'never'

def pull_policy_converter(value):
if isinstance(value, PullPolicy):
return value
try:
return PullPolicy(value)
except ValueError:
raise ValueError(f"Invalid pull policy: {value}")


@target_factory.reg_driver
@attr.s(eq=False)
class DockerDriver(PowerProtocol, Driver):
Expand All @@ -31,6 +60,8 @@ class DockerDriver(PowerProtocol, Driver):
bindings (dict): The labgrid bindings
Args passed to docker.create_container:
image_uri (str): The uri of the image to fetch
pull (str): Pull policy. Default policy is `always` for backward
compatibility concerns
command (str): The command to execute once container has been created
volumes (list): The volumes to declare
environment (list): Docker environment variables to set
Expand All @@ -42,6 +73,8 @@ class DockerDriver(PowerProtocol, Driver):
bindings = {"docker_daemon": {"DockerDaemon"}}
image_uri = attr.ib(default=None, validator=attr.validators.optional(
attr.validators.instance_of(str)))
pull = attr.ib(default=PullPolicy.Always,
converter=pull_policy_converter)
command = attr.ib(default=None, validator=attr.validators.optional(
attr.validators.instance_of(str)))
volumes = attr.ib(default=None, validator=attr.validators.optional(
Expand Down Expand Up @@ -73,7 +106,17 @@ def on_activate(self):
import docker
self._client = docker.DockerClient(
base_url=self.docker_daemon.docker_daemon_url)
self._client.images.pull(self.image_uri)

if self.pull == PullPolicy.Always:
self._client.images.pull(self.image_uri)
elif self.pull == PullPolicy.Missing:
try:
self._client.images.get(self.image_uri)
except docker.errors.ImageNotFound:
self._client.images.pull(self.image_uri)
elif self.pull == PullPolicy.Never:
self._client.images.get(self.image_uri)

self._container = self._client.api.create_container(
self.image_uri,
command=self.command,
Expand Down
82 changes: 81 additions & 1 deletion tests/test_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""

import pytest
import docker
import io

from labgrid import Environment
from labgrid.driver import DockerDriver
Expand Down Expand Up @@ -44,6 +46,34 @@ def docker_env(tmp_path_factory):
drivers:
- DockerDriver:
image_uri: "rastasheep/ubuntu-sshd:16.04"
pull: 'missing'
container_name: "ubuntu-lg-example"
host_config: {"network_mode": "bridge"}
network_services: [
{"port": 22, "username": "root", "password": "root"}]
- DockerStrategy: {}
- SSHDriver:
keyfile: ""
"""
)
return Environment(str(p))


@pytest.fixture
def docker_env_for_local_container(tmp_path_factory):
"""Create Environment instance from the given inline YAML file."""
p = tmp_path_factory.mktemp("docker") / "config.yaml"
p.write_text(
"""
targets:
main:
resources:
- DockerDaemon:
docker_daemon_url: "unix:///var/run/docker.sock"
drivers:
- DockerDriver:
image_uri: "local_rastasheep"
pull: "never"
container_name: "ubuntu-lg-example"
host_config: {"network_mode": "bridge"}
network_services: [
Expand Down Expand Up @@ -91,6 +121,25 @@ def command(docker_target):
strategy.transition("gone")


@pytest.fixture
def docker_target_for_local_image(docker_env_for_local_container):
"""Same as `docker_target` but uses a different image uri"""
t = docker_env_for_local_container.get_target()
yield t

from labgrid.resource import ResourceManager
ResourceManager.instances = {}


@pytest.fixture
def local_command(docker_target_for_local_image):
"""Same as `command` but uses a different image uri"""
strategy = docker_target_for_local_image.get_driver('DockerStrategy')
strategy.transition("accessible")
shell = docker_target_for_local_image.get_driver('CommandProtocol')
yield shell
strategy.transition("gone")

@pytest.mark.skipif(not check_external_progs_present(),
reason="No access to a docker daemon")
def test_docker_with_daemon(command):
Expand All @@ -110,6 +159,32 @@ def test_docker_with_daemon(command):
assert len(stderr) == 0


@pytest.fixture
def build_image():
client = docker.from_env()
dockerfile_content = """
FROM rastasheep/ubuntu-sshd:16.04
"""
dockerfile_stream = io.BytesIO(dockerfile_content.encode("utf-8"))
image, logs = client.images.build(fileobj=dockerfile_stream, tag="local_rastasheep", rm=True)


@pytest.mark.skipif(not check_external_progs_present(),
reason="No access to a docker daemon")
def test_docker_with_daemon_and_local_image(build_image, local_command):
"""Build a container locally and connect to it"""
stdout, stderr, return_code = local_command.run('cat /proc/version')
assert return_code == 0
assert len(stdout) > 0
assert len(stderr) == 0
assert 'Linux' in stdout[0]

stdout, stderr, return_code = local_command.run('false')
assert return_code != 0
assert len(stdout) == 0
assert len(stderr) == 0


def test_create_driver_fail_missing_docker_daemon(target):
"""The test target does not contain any DockerDaemon instance -
and so creation must fail.
Expand Down Expand Up @@ -159,6 +234,8 @@ def test_docker_without_daemon(docker_env, mocker):
'Id': '1'
}]
]
docker_client.images.get.side_effect = docker.errors.ImageNotFound(
"Image not found", response=None, explanation="")

# Mock actions on the imported "socket" python module
socket_create_connection = mocker.patch('socket.create_connection')
Expand Down Expand Up @@ -199,7 +276,10 @@ def test_docker_without_daemon(docker_env, mocker):
# Assert what mock calls transitioning to "shell" must have caused
#
# DockerDriver::on_activate():
assert docker_client.images.pull.call_count == 1
image_uri = t.get_driver('DockerDriver').image_uri
docker_client.images.get.assert_called_once_with(image_uri)
docker_client.images.pull.assert_called_once_with(image_uri)

assert api_client.create_host_config.call_count == 1
assert api_client.create_container.call_count == 1
#
Expand Down

0 comments on commit 72647bc

Please sign in to comment.