-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathdocker.py
More file actions
321 lines (265 loc) · 10.8 KB
/
Copy pathdocker.py
File metadata and controls
321 lines (265 loc) · 10.8 KB
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
319
320
321
# (C) Datadog, Inc. 2018-present
# All rights reserved
# Licensed under a 3-clause BSD style license (see LICENSE)
import os
from contextlib import contextmanager
from typing import Iterator # noqa: F401
from urllib.parse import urlparse
from .conditions import CheckDockerLogs
from .env import environment_run, get_state, save_state
from .fs import create_file, file_exists
from .spec import load_spec
from .structures import EnvVars, LazyFunction, TempDir
from .subprocess import run_command
from .utils import find_check_root
try:
from contextlib import ExitStack
except ImportError:
from contextlib2 import ExitStack
def get_docker_hostname():
"""
Determine the hostname Docker uses based on the environment, defaulting to `localhost`.
"""
return urlparse(os.getenv('DOCKER_HOST', '')).hostname or 'localhost'
def get_container_ip(container_id_or_name):
"""
Get a Docker container's IP address from its ID or name.
"""
command = [
'docker',
'inspect',
'-f',
'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}',
container_id_or_name,
]
return run_command(command, capture='out', check=True).stdout.strip()
def compose_file_active(compose_file):
"""
Returns a `bool` indicating whether or not a compose file has any active services.
"""
command = ['docker', 'compose', '-f', compose_file, 'ps']
lines = run_command(command, capture='out', check=True).stdout.strip().splitlines()
return len(lines) > 1
def using_windows_containers():
"""
Returns a `bool` indicating whether or not Docker is configured to use Windows containers.
"""
os_type = run_command(['docker', 'info', '--format', '{{.OSType}}'], capture=True, check=True).stdout.strip()
return os_type == 'windows'
@contextmanager
def shared_logs(example_log_configs, mount_whitelist=None):
log_source = example_log_configs[0].get('source', 'check')
if mount_whitelist is None:
# Default to all
mount_whitelist = range(1, len(example_log_configs) + 1)
env_vars = {}
docker_volumes = get_state('docker_volumes', [])
with ExitStack() as stack:
for i, example_log_config in enumerate(example_log_configs, 1):
if i not in mount_whitelist:
continue
log_name = 'dd_log_{}'.format(i)
d = stack.enter_context(TempDir(log_name))
# Create the file that will ultimately be shared by containers
shared_log_file = os.path.join(d, '{}_{}.log'.format(log_source, log_name))
if not file_exists(shared_log_file):
create_file(shared_log_file)
# Set config to the path in the Agent
agent_mount_path = '/var/log/{}/{}'.format(log_source, log_name)
example_log_config['path'] = agent_mount_path
docker_volumes.append('{}:{}'.format(shared_log_file, agent_mount_path))
# If service is the default, use the source
if example_log_config.get('service', '<SERVICE>') == '<SERVICE>':
example_log_config['service'] = log_source
# Make it available to reference for Docker volumes
env_vars[log_name.upper()] = shared_log_file
# Inject and persist this data for `env test` & `env stop`
save_state('logs_config', example_log_configs)
save_state('docker_volumes', docker_volumes)
with EnvVars(env_vars):
yield
@contextmanager
def docker_run(
compose_file=None,
waith_for_health=False,
build=False,
service_name=None,
up=None,
down=None,
on_error=None,
sleep=None,
endpoints=None,
log_patterns=None,
mount_logs=False,
conditions=None,
env_vars=None,
wrappers=None,
attempts=None,
attempts_wait=1,
capture=None,
):
"""
A convenient context manager for safely setting up and tearing down Docker environments.
Parameters:
compose_file (str):
A path to a Docker compose file. A custom tear
down is not required when using this.
waith_for_health (bool):
Whether or not to wait for the health of the service to be healthy before yielding.
build (bool):
Whether or not to build images for when `compose_file` is provided
service_name (str):
Optional name for when ``compose_file`` is provided
up (callable):
A custom setup callable
down (callable):
A custom tear down callable. This is required when using a custom setup.
on_error (callable):
A callable called in case of an unhandled exception
sleep (float):
Number of seconds to wait before yielding. This occurs after all conditions are successful.
endpoints (list[str]):
Endpoints to verify access for before yielding. Shorthand for adding
`CheckEndpoints(endpoints)` to the `conditions` argument.
log_patterns (list[str | re.Pattern]):
Regular expression patterns to find in Docker logs before yielding.
This is only available when `compose_file` is provided. Shorthand for adding
`CheckDockerLogs(compose_file, log_patterns, 'all')` to the `conditions` argument.
mount_logs (bool):
Whether or not to mount log files in Agent containers based on example logs configuration
conditions (callable):
A list of callable objects that will be executed before yielding to check for errors
env_vars (dict[str, str]):
A dictionary to update `os.environ` with during execution
wrappers (list[callable]):
A list of context managers to use during execution
attempts (int):
Number of attempts to run `up` and the `conditions` successfully. Defaults to 2 in CI
attempts_wait (int):
Time to wait between attempts
"""
if compose_file and up:
raise TypeError('You must select either a compose file or a custom setup callable, not both.')
if compose_file is not None:
if not isinstance(compose_file, str):
raise TypeError('The path to the compose file is not a string: {}'.format(repr(compose_file)))
composeFileArgs = {'compose_file': compose_file, 'build': build, 'service_name': service_name}
if capture is not None:
composeFileArgs['capture'] = capture
if waith_for_health:
composeFileArgs['waith_for_health'] = waith_for_health
set_up = ComposeFileUp(**composeFileArgs)
if down is not None:
tear_down = down
else:
tear_down = ComposeFileDown(compose_file)
if on_error is None:
on_error = ComposeFileLogs(compose_file)
else:
set_up = up
tear_down = down
docker_conditions = []
if log_patterns is not None:
if compose_file is None:
raise ValueError(
'The `log_patterns` convenience is unavailable when using '
'a custom setup. Please use a custom condition instead.'
)
docker_conditions.append(CheckDockerLogs(compose_file, log_patterns, 'all'))
if conditions is not None:
docker_conditions.extend(conditions)
wrappers = list(wrappers) if wrappers is not None else []
if mount_logs:
if isinstance(mount_logs, dict):
wrappers.append(shared_logs(mount_logs['logs']))
# Easy mode, read example config
else:
# An extra level deep because of the context manager
check_root = find_check_root(depth=2)
example_log_configs = _read_example_logs_config(check_root)
if mount_logs is True:
wrappers.append(shared_logs(example_log_configs))
elif isinstance(mount_logs, (list, set)):
wrappers.append(shared_logs(example_log_configs, mount_whitelist=mount_logs))
else:
raise TypeError(
'mount_logs: expected True, a list or a set, but got {}'.format(type(mount_logs).__name__)
)
with environment_run(
up=set_up,
down=tear_down,
on_error=on_error,
sleep=sleep,
endpoints=endpoints,
conditions=docker_conditions,
env_vars=env_vars,
wrappers=wrappers,
attempts=attempts,
attempts_wait=attempts_wait,
) as result:
yield result
class ComposeFileUp(LazyFunction):
def __init__(
self,
compose_file: str,
build: bool = False,
service_name: str | None = None,
capture: str | None = None,
waith_for_health: bool = False,
):
self.compose_file = compose_file
self.build = build
self.service_name = service_name
self.capture = capture
self.waith_for_health = waith_for_health
self.command = ['docker', 'compose', '-f', self.compose_file, 'up', '-d', '--force-recreate']
if waith_for_health:
self.command.append('--wait')
if self.build:
self.command.append('--build')
if self.service_name:
self.command.append(self.service_name)
def __call__(self):
args = {'check': True}
if self.capture is not None:
args['capture'] = self.capture
return run_command(self.command, **args)
class ComposeFileLogs(LazyFunction):
def __init__(self, compose_file, check=True):
self.compose_file = compose_file
self.check = check
self.command = ['docker', 'compose', '-f', self.compose_file, 'logs']
def __call__(self, exception):
return run_command(self.command, capture=False, check=self.check)
class ComposeFileDown(LazyFunction):
def __init__(self, compose_file, check=True):
self.compose_file = compose_file
self.check = check
self.command = [
'docker',
'compose',
'-f',
self.compose_file,
'down',
'--volumes',
'--remove-orphans',
'-t',
'0',
]
def __call__(self):
return run_command(self.command, check=self.check)
def _read_example_logs_config(check_root):
spec = load_spec(check_root)
for f in spec['files']:
for option in f['options']:
if option.get('template') == 'logs':
return option['example']
raise ValueError('No logs example found')
@contextmanager
def temporarily_stop_service(service, compose_file, check=True):
# type: (str, str, bool) -> Iterator[None]
stop_command = ['docker', 'compose', '-f', compose_file, 'stop', service]
start_command = ['docker', 'compose', '-f', compose_file, 'start', service]
run_command(stop_command, capture=False, check=check)
yield
run_command(start_command, capture=False, check=check)