-
Notifications
You must be signed in to change notification settings - Fork 199
/
Copy pathtest_docker.py
318 lines (273 loc) · 11.8 KB
/
test_docker.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
"""
Test labgrid code of communicating with a docker daemon and using it
for creating, starting and accessing a docker container.
"""
import pytest
import docker
import io
from labgrid import Environment
from labgrid.driver import DockerDriver
from labgrid.resource.docker import DockerConstants
from labgrid.exceptions import NoResourceFoundError
pytest.importorskip("docker")
def check_external_progs_present():
"""Determine if host machine has a usable docker daemon"""
try:
import docker
try:
dock = docker.from_env()
dock.info()
except docker.errors.DockerException:
return False
except OSError:
return False
return True
@pytest.fixture
def docker_env(tmp_path_factory):
"""Create Environment instance from the given inline YAML file."""
p = tmp_path_factory.mktemp("docker") / "config.yaml"
# Note: The SSHDriver part at bottom is only used by the test that
# will run if a docker daemon is present, not by
# test_driver_without_daemon.
p.write_text(
"""
targets:
main:
resources:
- DockerDaemon:
docker_daemon_url: "unix:///var/run/docker.sock"
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: [
{"port": 22, "username": "root", "password": "root"}]
- DockerStrategy: {}
- SSHDriver:
keyfile: ""
"""
)
return Environment(str(p))
@pytest.fixture
def docker_target(docker_env):
"""Get a labgrid Target instance from the Environment instance
given by docker_env. When tearing down the Target instance, make sure
singleton ResourceManager is "reset".
"""
t = docker_env.get_target()
yield t
# Fake! In real life, ResourceManager is intended to be a singleton.
# The class is created only once - when python parses common.py.
# But this means that the class with its "instances" attribute survives
# from test case to test case. This is not what we want. On the contrary,
# we want each of test_docker_with_daemon and test_docker_without_daemon
# to run with a *fresh* instance of the ResourceManager singleton.
#
# Luckily it is easy to "reset" ResourceManager. The singleton is kept
# in attribute "instances" so by resetting "instances" to {}, next test
# case will force creation of a fresh ResourceManager instance.
from labgrid.resource import ResourceManager
ResourceManager.instances = {}
@pytest.fixture
def command(docker_target):
"""Bring system to a state where it's possible to execute commands
on a running docker container. When done, stop the container again.
"""
strategy = docker_target.get_driver('DockerStrategy')
strategy.transition("accessible")
shell = docker_target.get_driver('CommandProtocol')
yield shell
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):
"""Test the docker machinery when a running docker daemon can be used
(thus test is skipped if there is no such daemon). The tests executes
a few tests inside a running docker container using SSHDriver for access.
"""
stdout, stderr, return_code = 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 = command.run('false')
assert return_code != 0
assert len(stdout) == 0
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.
"""
with pytest.raises(NoResourceFoundError):
DockerDriver(target, "docker_driver")
def test_docker_without_daemon(docker_env, mocker):
"""Test as many aspects as possible of DockerDriver, DockerDaemon,
DockerManager and DockerStrategy without using an actual
docker daemon, real sockets or system time"""
# Target::update_resources() and Target::await_resources use
# time.monotonic() and time.sleep() to control when to search
# for resources. Avoid time delays and make running from cmd-line
# and inside debugger equal by mocking out all time.
time_monotonic = mocker.patch('labgrid.target.monotonic')
time_monotonic.side_effect = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
# Mock actions on the imported "docker" python module
docker_client_class = mocker.patch('docker.DockerClient',
autospec=True)
docker_client = docker_client_class.return_value
api_client_class = mocker.patch('docker.api.client.APIClient',
autospec=True)
docker_client.api = api_client_class.return_value
api_client = docker_client.api
api_client.base_url = "unix:///var/run/docker.sock"
# First, a "mocked" old docker container is returned by
# ...api.containers(); this is done when DockerDaemon tries
# to clean up old containers. Next, a one-item list is delivered by
# ...api.containers() which is part of DockerDaemon::update_resources()
# - it is cached for future use; therefore no need to replicate
# this entry in the side_effects list.
api_client.containers.side_effect = [
[{'Labels': {DockerConstants.DOCKER_LG_CLEANUP_LABEL:
DockerConstants.DOCKER_LG_CLEANUP_TYPE_AUTO},
'NetworkSettings': {'IPAddress': '1.1.1.1'},
'Names': 'old-one',
'Id': '0'
}],
[{'Labels': {DockerConstants.DOCKER_LG_CLEANUP_LABEL:
DockerConstants.DOCKER_LG_CLEANUP_TYPE_AUTO},
'NetworkSettings': {'IPAddress': '2.1.1.1'},
'Names': 'actual-one',
'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')
sock = mocker.MagicMock()
# First two negative connection setup attempts are used at initial
# resource setup during strategy.transition("shell"); these simulate
# that it takes time for the docker container to come up. The final,
# successful, return value is delivered when t.update_resources()
# is called explicitly later on.
socket_create_connection.side_effect = [
Exception('No connection on first call'),
Exception('No connection on second call'),
sock]
# get_target() - which calls make_target() - creates resources/drivers
# from .yaml configured environment. Creation provokes binding
# and attempts to connect to network services.
api_client.remove_container.assert_not_called()
t = docker_env.get_target()
assert api_client.remove_container.call_count == 1
# Make sure DockerDriver didn't accidentally succeed with a socket connect
# attempt (this fact is actually expressed by what happens next -
# the socket is closed).
sock.shutdown.assert_not_called()
sock.close.assert_not_called()
# Get strategy - needed to transition to "shell" state.
strategy = t.get_driver("DockerStrategy")
# strategy starts in state "unknown" so the following should be a no-op.
strategy.transition("unknown")
# Now activate DockerDriver and set it "on". This creates and starts
# a (mocked) container.
strategy.transition("accessible")
# Assert what mock calls transitioning to "shell" must have caused
#
# DockerDriver::on_activate():
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
#
# DockerDriver::on()
assert api_client.start.call_count == 1
# From here the test using the real docker daemon would proceed with
# shell = t.get_driver('CommandProtocol')
# shell.run('...')
# which makes use of e.g. the SSHDriver. Binding the SSHDriver
# is important since it triggers activation of the NetworkService.
# But then SSHDriver uses ssh to connect to the NetworkService
# which will lead to error. Instead just call update_resources()
# directly - which is what is needed to provoke DockerDaemon to create
# a new NetworkService instance.
t.update_resources()
# This time socket connection was successful
# (per the third socket_create_connection return value defined above).
assert sock.shutdown.call_count == 1
assert sock.close.call_count == 1
# Bonus: Test what happens if taking a forbidden strategy transition:
# "shell" -> "unknown".
from labgrid.strategy import StrategyError
with pytest.raises(StrategyError):
strategy.transition("unknown")
# Also bonus: How are invalid state names handled?
with pytest.raises(KeyError):
strategy.transition("this is not a valid state")
# Return to "gone" state - to also use that part of the DockerDriver code.
strategy.transition("gone")
from labgrid.strategy.dockerstrategy import Status
assert strategy.status == Status.gone