forked from mobilecoinfoundation/mobilecoin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmob
executable file
·427 lines (358 loc) · 15.9 KB
/
mob
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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
#!/usr/bin/env python3
# Copyright (c) 2018-2020 MobileCoin Inc.
"""
A helper tool for getting the dockerized build environment
`mob` tool helps you get a prompt in the same Docker environment used in CI,
to make building easier.
operation
---------
The basic operation is like this:
0. `./mob prompt` is invoked
1. Read .mobconf to find the Dockerfile and Dockerfile-version file, and the remote
source for the image to use for caching.
2. Pull the remote version if available, or the previous if not available. (This
is if you want to make changes to the image locally.)
3. Try to build the Dockerfile, using the version that we pulled as a cache source.
If you didn't change it then this completes instantly.
4. Do an appropriate form of `docker run -it bash` in this image, mounting the
root of the repository to `/tmp/mobilenode` and setting that as the working directory.
There are some flags and options for modifying this process, e.g. `image` just builds
the image and doesn't run it or give you a prompt. `--dry-run` just
shows you the shell commands without executing them. `--no-pull` means you don't
pull a docker image at all.
`mob` tool attemps to mount the `/dev/isgx` device correctly if it is avialable
and you selected `--hw` mode, so that you can run tests in hardware mode.
usage notes (ssh)
-----------------
The `--ssh-dir` and `--ssh-agent` options can be used if the build requires
access to private repos on github (mobilecoin repo sources will be mounted so
ssh stuff is not needed for that, this is for any private dependencies of mobilcoin
repo.) If provided, these options will try to get your credentials from the host
enviornment into the container so that cargo can pull.
usage notes (changing the Dockerfile)
-------------------------------------
When you make changes to the Dockerfile, you should increment the minor version,
and commit that alongside the changes.
Previous minor versions are used as cache sources. Caching never occurs across
major version upgrade so this is a way to ensure that security updates are pulled in
over time.
The major version number is treated as a string -- it doesn't have to be a number,
if you have a long-lived release branch in git-flow and need to patch the dockerfile,
you may want to change the major version to some string to make an independent fork
of dockerfile versions, e.g. `10_15` -> `release-04-09-2010_0`
The farm only publishes versions when they are merged to master, but your local
builds of the images are also used as caches. If you want to prevent pulling from
the farm entirely use `--no-pull`.
mobconf
-------
`mob` supports configuration via the `.mobconf` file. This allows it to build multiple
different projects that exist in the same repository.
`mob` attempts to find a `.mobconf` file by starting in `pwd` and searching up,
it is an error if it can't find one.
.mobconf sections:
[image]
dockerfile = path to dockerfile. build context is the dir of dockerfile
target = layer in dockerfile that is targetted
repository = storage for remote images from farm e.g. gcr.io/mobilenode-211420 (optional)
"""
import argparse
import configparser
import os
import pathlib
import platform
import subprocess
import sys
parser = argparse.ArgumentParser(prog = "mob", description = "Perform an action or get a prompt in docker build environment")
parser.add_argument("action", choices = ["prompt", "image", "default-tag"], help = """
(prompt) Run bash in the build environment
(image) Only create the builder image, don't even run bash
(default-tag) Print on stdout the default image tag, to support automation
""")
parser.add_argument("container_name", nargs='?', default=None, help = "The name for the container, run with prompt")
parser.add_argument("--hw", action = "store_true", help = "Set SGX_MODE=HW. Default is SGX_MODE=SW")
parser.add_argument("--ias-prod", action = "store_true", help = "Set IAS_MODE=PROD. Default is IAS_MODE=DEV. This affects which IAS endpoints we use.")
parser.add_argument("--dry-run", action = "store_true", help = "Don't run docker, show how we would invoke docker.")
parser.add_argument("--tag", default = None, type=str, help = "Use given tag for builder image rather than the autogenerated one. (Also cache from here)")
parser.add_argument("--no-pull", action = "store_true", help = "By default, we attempt to pull latest builder-install from gcr.io (cloudbuild), and use it for caching. Disables that.")
parser.add_argument("--verbose", action = "store_true", help = "Show the commands on stdout. True by default when noninteractive, implied by dry-run")
parser.add_argument("--ssh-dir", action='store_true', help='Mount $HOME/.ssh directory into /root/.ssh for ssh key access')
parser.add_argument("--ssh-agent", action='store_true', help='Use ssh-agent on host machine for ssh authentication. Also controlled by SSH_AUTH_SOCK env variable')
parser.add_argument("--expose", nargs='+', default=None, help = "Any additional ports to expose")
args = parser.parse_args()
###
# Implement forced settings
###
if args.dry_run or not sys.stdout.isatty():
args.verbose = True
###
# Implement verbose, dry_run settings
###
# When python is invoked from docker in CI, we won't see anything because of
# buffered output unless we flush. It's hard to ensure PYTHONUNBUFFERED=0 or -u
# is used consistently.
def eprint(*argv, **kwargs):
print(*argv, file=sys.stderr, **kwargs)
sys.stderr.flush()
# vprint is eprint that only happens in verbose mode
def vprint(*argv, **kwargs):
if args.verbose:
eprint(*argv, **kwargs)
# Run a command, unless we are in dry_run mode and should not do that
# Print the command if in verbose mode
def maybe_run(command):
vprint(command)
if not args.dry_run:
subprocess.check_call(command)
# Change directory.
# Print the command if in verbose mode
def verbose_chdir(path):
vprint("cd", path)
os.chdir(path)
# Check if we have a given docker tag
def have_tag(arg):
try:
cmd = ["docker", "images", "-q", arg]
vprint(cmd)
return len(subprocess.check_output(cmd)) > 0
except subprocess.CalledProcessError:
eprint("Error searching for image: " + arg)
return False
# Check if we have a git commit and compute outside the container
# Sometimes git cannot be used in the container, if building in public dir,
# or if you used git worktree to make a new worktree.
def get_git_commit():
if "GIT_COMMIT" in os.environ:
return os.environ["GIT_COMMIT"]
else:
try:
cmd = ["git", "describe", "--always", "--dirty=-modified"]
vprint(cmd)
return subprocess.check_output(cmd)[:-1].decode()
except subprocess.CalledProcessError:
eprint("Couldn't get git revision")
return None
##
# Environment checks
##
if "SSH_AUTH_SOCK" in os.environ:
if platform.system().lower() != "linux":
vprint("SSH Auth Socket found, but not running on linux")
args.ssh_dir = True
else:
vprint("Mapping SSH_AUTH_SOCKET into container")
args.ssh_agent = True
# Find work directory and change directory there
# This is based on searching for nearest .mobconf file, moving upwards from cwd
top_level = os.getcwd()
while not os.path.exists(os.path.join(top_level, ".mobconf")):
new_top_level = os.path.dirname(top_level)
if new_top_level == top_level:
print("fatal: could not find .mobconf")
sys.exit(1)
top_level = new_top_level
mobconf = configparser.ConfigParser()
mobconf.read(".mobconf")
verbose_chdir(top_level)
# Find and parse dockerfile version file
docker_dir = os.path.dirname(mobconf['image']['dockerfile'])
docker_file = os.path.basename(mobconf['image']['dockerfile'])
docker_ver_file = os.path.join(docker_dir, 'Dockerfile-version')
with open(os.path.join(top_level, docker_ver_file), 'r') as file:
data = file.read().strip()
ver_segments = data.split('_')
if len(ver_segments) != 2:
raise Exception("Expected form A_B for Dockerfile-version, found '" + data + "' which parsed as: " + str(ver_segments))
major_ver = ver_segments[0]
minor_ver = int(ver_segments[1])
if minor_ver < 0:
raise Exception("Invalid minor ver: " + minor_ver)
# Calculate default tag.
repo = mobconf['image']['repository'] if 'repository' in mobconf['image'] else ""
default_tag = os.path.join(repo, mobconf['image']['target'] + ':' + major_ver + "_" + str(minor_ver))
parent_tag = None
if minor_ver > 0:
parent_tag = os.path.join(repo, mobconf['image']['target'] + ':' + major_ver + "_" + str(minor_ver - 1))
if args.action == "default-tag":
print(default_tag, end = '')
sys.exit(0)
###
# Calculate command
###
build_env = [
"RUST_BACKTRACE=full",
"SGX_MODE=HW" if args.hw else "SGX_MODE=SW",
"IAS_MODE=PROD" if args.ias_prod else "IAS_MODE=DEV",
]
# Calculate bash commands
if args.action == "prompt" or args.action == "image":
bash_commands = None
else:
raise Exception('Unknown action: ' + args.action)
###
# docker build
###
if args.tag is None:
tag = default_tag
else:
tag = args.tag
# Pull cache sources from repository if we have one
if 'repository' in mobconf['image']:
if not args.no_pull:
try:
maybe_run(["docker", "image", "pull", default_tag])
except:
eprint("Warning: Could not pull from: ", default_tag)
if parent_tag is not None:
try:
maybe_run(["docker", "image", "pull", parent_tag])
except:
eprint("Warning: Could not pull from: ", parent_tag)
else:
vprint('No repository found in .mobconf to pull from')
# Don't build the image if we already have it, unless
# image action was specified (user requested the image)
#
# Note: IMO it should be okay to always rebuild the image as long as the farm is
# giving us a good cache source for it, but it seems that we always get a cache
# miss at `COPY rust-toolchain .`, and I'm not sure why.
# This `if` here is me giving up on trying to resolve those layer cache miss issues.
# I think that it comes down to, in cloudbuild the permissions (user / group)
# are slightly different from what they are locally, and I don't want to spend more time on it.
# It would be nice if the docker image itself were actually reproducible, i.e. counts
# as a cache hit for itself whether it were built locally or in farm... sigh
if args.action == "image" or not have_tag(tag):
# Get an up-to-date image, even if repository is messed up
# Invoke docker from directory of dockerfile
# This seems to fix docker's layer caching when we are building the image variously
# from pwd = / and pwd = /public/.
# Please don't factor out the chdir without testing that this caching is still working
verbose_chdir(docker_dir)
docker_build = ["docker",
"build",
"--file", docker_file,
# build context is dir of Dockerfile
".",
"--target", mobconf['image']['target'],
"--tag", tag,
"--cache-from", tag,
"--cache-from", default_tag]
# N.B. --cache-from order on the line establishes priority:
# https://stackoverflow.com/questions/54574821/docker-build-not-using-cache-when-copying-gemfile-while-using-cache-from/56024061#56024061
if parent_tag is not None:
docker_build.extend(["--cache-from", parent_tag])
maybe_run(docker_build)
if args.action == "image":
sys.exit(0)
verbose_chdir(top_level)
###
# Implement mount-through option
###
# Compute mount_point, mount_from, and workdir
mount_point = "/tmp/mobilenode"
workdir = mount_point
mount_from = top_level
##
# Calculate docker run flags
##
# docker-run parameters:
docker_run = ["docker",
"run",
# container doesn't know mount_point so we have to set this
"--env", "CARGO_HOME=" + mount_point + "/cargo",
"--volume", mount_from + ":" + mount_point,
"--workdir", workdir,]
# Add /dev/isgx if HW mode, and it is available
# Mimicking bash checks `if [ -c /dev/isgx ]`
# The purpose of this check is that it is possible to build in HW mode, and even
# to run many tests, without installing sgx on the host and enabling this device in docker,
# but sometimes you really do need it, if you want to run consensus nodes etc.
# The friendliest thing seems to be, pass on the device if possible, maybe print a warning if not.
if args.hw:
if pathlib.Path("/dev/isgx").is_char_device():
docker_run.extend(["--device", "/dev/isgx"])
else:
eprint("Did not find /dev/isgx on the host, skipping")
# Add build environment
for pair in build_env:
docker_run.extend(["--env", pair])
# Add GIT_COMMIT if present
git_commit = get_git_commit()
if git_commit is not None:
docker_run.extend(["--env", "GIT_COMMIT=" + git_commit])
# Enable sccache usage if sccache dir is found on the host.
# We do this by mounting the dir into the container, and setting the sccache
# environment variable, which can be picked up by Makefile. sccache is already
# installed in the container.
#
# An alternative might be to run the sccache server outside the container, and
# expose port 4226 so that they can talk, per
# https://github.com/mozilla/sccache/blob/master/docs/Jenkins.md
#
# This tool is not used in jenkins anymore so we don't do that
host_sccache_dir = os.path.expanduser("~/.cache/sccache") # per docs this is the default for sccache
if "SCCACHE_DIR" in os.environ:
host_sccache_dir = os.environ["SCCACHE_DIR"]
if os.path.isdir(host_sccache_dir):
docker_run.extend([
"--env", "SCCACHE=/root/.cargo/bin/sccache",
"--volume", host_sccache_dir + ":" + "/root/.cache/sccache",
])
# If running interactively (with a tty), get a tty in the container also
# This allows colored build logs when running locally without messing up logs in CI
if sys.stdout.isatty():
docker_run.extend(["-t"])
# in prompt, use -i to get user input
if args.action == "prompt":
docker_run.extend(["-i"])
# ports might be exposed to run clients or a local network
if args.action == "prompt":
docker_run.extend([
"--expose", "8080",
"--expose", "8081",
"--expose", "8443",
"--expose", "3223",
"--expose", "3225",
"--expose", "3226",
"--expose", "3228",
"--expose", "4444",
])
if args.expose is not None:
for port in args.expose:
docker_run.extend(["--expose", port])
# Map in the ssh directory, or the ssh-agent socket
if args.ssh_dir:
eprint("SSH Agent authentication disabled. You will need to run the following commands to build: ")
eprint('eval `ssh-agent`')
eprint('ssh-add /root/.ssh/<your-ssh-private-key>')
docker_run.extend(["--volume", os.path.expanduser("~/.ssh") + ":" + "/root/.ssh"])
elif args.ssh_agent:
docker_run.extend(["--env", "SSH_AUTH_SOCK=/tmp/ssh_auth_sock"])
docker_run.extend(["--volume", os.environ["SSH_AUTH_SOCK"] + ":" + "/tmp/ssh_auth_sock"])
# debug options allow you to attach gdb when debugging failing tests
if args.action == "prompt":
docker_run.extend([
"--cap-add", "SYS_PTRACE",
])
# Name the container if a name was provided
if args.container_name is not None:
docker_run.extend([
"--name", args.container_name,
])
# Add image name and command
docker_run.extend([tag])
command = ["/bin/bash"]
if bash_commands is not None:
command.extend(["-e", "-c"])
command.extend(["; ".join(bash_commands)])
docker_run.extend(command)
try:
maybe_run(docker_run)
except subprocess.CalledProcessError as exception:
if args.action == 'prompt' and exception.returncode == 130:
# This is normal exit of prompt
sys.exit(0)
raise # rethrow
finally:
# cleanup named container on exit
if args.container_name is not None:
maybe_run(["docker", "rm", "-f", args.container_name])