Skip to content

Commit 587a872

Browse files
authored
Merge pull request #1533 from b2vn/local-docker
add option to use local docker image
2 parents e821d5a + 72647bc commit 587a872

File tree

3 files changed

+136
-2
lines changed

3 files changed

+136
-2
lines changed

doc/configuration.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3123,12 +3123,23 @@ Implements:
31233123
31243124
DockerDriver:
31253125
image_uri: 'rastasheep/ubuntu-sshd:16.04'
3126+
pull: 'always'
31263127
container_name: 'ubuntu-lg-example'
31273128
host_config: {'network_mode': 'bridge'}
31283129
network_services: [{'port': 22, 'username': 'root', 'password': 'root'}]
31293130
31303131
Arguments:
31313132
- image_uri (str): identifier of the docker image to use (may have a tag suffix)
3133+
- pull (str): pull policy, supports "always", "missing", "never". Default is
3134+
"always"
3135+
3136+
- always: Always pull the image and throw an error if the pull fails.
3137+
- missing: Pull the image only when the image is not in the local
3138+
containers storage. Throw an error if no image is found and the pull
3139+
fails.
3140+
- never: Never pull the image but use the one from the local containers
3141+
storage. Throw a `docker.errors.ImageNotFound` if no image is found.
3142+
31323143
- command (str): optional, command to run in the container (depends on image)
31333144
- volumes (list): optional, list to configure volumes mounted inside the container
31343145
- container_name (str): name of the container

labgrid/driver/dockerdriver.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Class for connecting to a docker daemon running on the host machine.
33
"""
4+
from enum import Enum
5+
46
import attr
57

68
from labgrid.factory import target_factory
@@ -9,6 +11,33 @@
911
from labgrid.protocol.powerprotocol import PowerProtocol
1012

1113

14+
class PullPolicy(Enum):
15+
"""Pull policy for the `DockerDriver`.
16+
17+
Modelled after `podman run --pull` / `docker run --pull`.
18+
19+
* always: Always pull the image and throw an error if the pull fails.
20+
* missing: Pull the image only when the image is not in the local
21+
containers storage. Throw an error if no image is found and the pull
22+
fails.
23+
* never: Never pull the image but use the one from the local containers
24+
storage. Throw an error if no image is found.
25+
* newer: **Note** not supported by the driver, and therefore not
26+
implemented.
27+
"""
28+
Always = 'always'
29+
Missing = 'missing'
30+
Never = 'never'
31+
32+
def pull_policy_converter(value):
33+
if isinstance(value, PullPolicy):
34+
return value
35+
try:
36+
return PullPolicy(value)
37+
except ValueError:
38+
raise ValueError(f"Invalid pull policy: {value}")
39+
40+
1241
@target_factory.reg_driver
1342
@attr.s(eq=False)
1443
class DockerDriver(PowerProtocol, Driver):
@@ -31,6 +60,8 @@ class DockerDriver(PowerProtocol, Driver):
3160
bindings (dict): The labgrid bindings
3261
Args passed to docker.create_container:
3362
image_uri (str): The uri of the image to fetch
63+
pull (str): Pull policy. Default policy is `always` for backward
64+
compatibility concerns
3465
command (str): The command to execute once container has been created
3566
volumes (list): The volumes to declare
3667
environment (list): Docker environment variables to set
@@ -42,6 +73,8 @@ class DockerDriver(PowerProtocol, Driver):
4273
bindings = {"docker_daemon": {"DockerDaemon"}}
4374
image_uri = attr.ib(default=None, validator=attr.validators.optional(
4475
attr.validators.instance_of(str)))
76+
pull = attr.ib(default=PullPolicy.Always,
77+
converter=pull_policy_converter)
4578
command = attr.ib(default=None, validator=attr.validators.optional(
4679
attr.validators.instance_of(str)))
4780
volumes = attr.ib(default=None, validator=attr.validators.optional(
@@ -73,7 +106,17 @@ def on_activate(self):
73106
import docker
74107
self._client = docker.DockerClient(
75108
base_url=self.docker_daemon.docker_daemon_url)
76-
self._client.images.pull(self.image_uri)
109+
110+
if self.pull == PullPolicy.Always:
111+
self._client.images.pull(self.image_uri)
112+
elif self.pull == PullPolicy.Missing:
113+
try:
114+
self._client.images.get(self.image_uri)
115+
except docker.errors.ImageNotFound:
116+
self._client.images.pull(self.image_uri)
117+
elif self.pull == PullPolicy.Never:
118+
self._client.images.get(self.image_uri)
119+
77120
self._container = self._client.api.create_container(
78121
self.image_uri,
79122
command=self.command,

tests/test_docker.py

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
"""
55

66
import pytest
7+
import docker
8+
import io
79

810
from labgrid import Environment
911
from labgrid.driver import DockerDriver
@@ -44,6 +46,34 @@ def docker_env(tmp_path_factory):
4446
drivers:
4547
- DockerDriver:
4648
image_uri: "rastasheep/ubuntu-sshd:16.04"
49+
pull: 'missing'
50+
container_name: "ubuntu-lg-example"
51+
host_config: {"network_mode": "bridge"}
52+
network_services: [
53+
{"port": 22, "username": "root", "password": "root"}]
54+
- DockerStrategy: {}
55+
- SSHDriver:
56+
keyfile: ""
57+
"""
58+
)
59+
return Environment(str(p))
60+
61+
62+
@pytest.fixture
63+
def docker_env_for_local_container(tmp_path_factory):
64+
"""Create Environment instance from the given inline YAML file."""
65+
p = tmp_path_factory.mktemp("docker") / "config.yaml"
66+
p.write_text(
67+
"""
68+
targets:
69+
main:
70+
resources:
71+
- DockerDaemon:
72+
docker_daemon_url: "unix:///var/run/docker.sock"
73+
drivers:
74+
- DockerDriver:
75+
image_uri: "local_rastasheep"
76+
pull: "never"
4777
container_name: "ubuntu-lg-example"
4878
host_config: {"network_mode": "bridge"}
4979
network_services: [
@@ -91,6 +121,25 @@ def command(docker_target):
91121
strategy.transition("gone")
92122

93123

124+
@pytest.fixture
125+
def docker_target_for_local_image(docker_env_for_local_container):
126+
"""Same as `docker_target` but uses a different image uri"""
127+
t = docker_env_for_local_container.get_target()
128+
yield t
129+
130+
from labgrid.resource import ResourceManager
131+
ResourceManager.instances = {}
132+
133+
134+
@pytest.fixture
135+
def local_command(docker_target_for_local_image):
136+
"""Same as `command` but uses a different image uri"""
137+
strategy = docker_target_for_local_image.get_driver('DockerStrategy')
138+
strategy.transition("accessible")
139+
shell = docker_target_for_local_image.get_driver('CommandProtocol')
140+
yield shell
141+
strategy.transition("gone")
142+
94143
@pytest.mark.skipif(not check_external_progs_present(),
95144
reason="No access to a docker daemon")
96145
def test_docker_with_daemon(command):
@@ -110,6 +159,32 @@ def test_docker_with_daemon(command):
110159
assert len(stderr) == 0
111160

112161

162+
@pytest.fixture
163+
def build_image():
164+
client = docker.from_env()
165+
dockerfile_content = """
166+
FROM rastasheep/ubuntu-sshd:16.04
167+
"""
168+
dockerfile_stream = io.BytesIO(dockerfile_content.encode("utf-8"))
169+
image, logs = client.images.build(fileobj=dockerfile_stream, tag="local_rastasheep", rm=True)
170+
171+
172+
@pytest.mark.skipif(not check_external_progs_present(),
173+
reason="No access to a docker daemon")
174+
def test_docker_with_daemon_and_local_image(build_image, local_command):
175+
"""Build a container locally and connect to it"""
176+
stdout, stderr, return_code = local_command.run('cat /proc/version')
177+
assert return_code == 0
178+
assert len(stdout) > 0
179+
assert len(stderr) == 0
180+
assert 'Linux' in stdout[0]
181+
182+
stdout, stderr, return_code = local_command.run('false')
183+
assert return_code != 0
184+
assert len(stdout) == 0
185+
assert len(stderr) == 0
186+
187+
113188
def test_create_driver_fail_missing_docker_daemon(target):
114189
"""The test target does not contain any DockerDaemon instance -
115190
and so creation must fail.
@@ -159,6 +234,8 @@ def test_docker_without_daemon(docker_env, mocker):
159234
'Id': '1'
160235
}]
161236
]
237+
docker_client.images.get.side_effect = docker.errors.ImageNotFound(
238+
"Image not found", response=None, explanation="")
162239

163240
# Mock actions on the imported "socket" python module
164241
socket_create_connection = mocker.patch('socket.create_connection')
@@ -199,7 +276,10 @@ def test_docker_without_daemon(docker_env, mocker):
199276
# Assert what mock calls transitioning to "shell" must have caused
200277
#
201278
# DockerDriver::on_activate():
202-
assert docker_client.images.pull.call_count == 1
279+
image_uri = t.get_driver('DockerDriver').image_uri
280+
docker_client.images.get.assert_called_once_with(image_uri)
281+
docker_client.images.pull.assert_called_once_with(image_uri)
282+
203283
assert api_client.create_host_config.call_count == 1
204284
assert api_client.create_container.call_count == 1
205285
#

0 commit comments

Comments
 (0)