This repository was archived by the owner on Oct 2, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 60
/
Copy pathch-image.py.in
336 lines (305 loc) · 14 KB
/
ch-image.py.in
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
#!%PYTHON_SHEBANG%
import argparse
import inspect
import os.path
import sys
ch_lib = os.path.dirname(os.path.abspath(__file__)) + "/../lib"
sys.path.insert(0, ch_lib)
import build_cache as bu
import charliecloud as ch
import build
import misc
import filesystem as fs
import pull
import push
## Constants ##
# FIXME: It’s currently easy to get the ch-run path from another script, but
# hard from something in lib. So, we set it here for now.
ch.CH_BIN = os.path.dirname(os.path.abspath(
inspect.getframeinfo(inspect.currentframe()).filename))
ch.CH_RUN = ch.CH_BIN + "/ch-run"
## Main ##
def main():
if (not os.path.exists(ch.CH_RUN)):
ch.depfails.append(("missing", ch.CH_RUN))
ap = ch.ArgumentParser(
description="Build and manage images; completely unprivileged.",
epilog="""Storage directory is used for caching and temporary images.
Location: first defined of --storage, $CH_IMAGE_STORAGE, and
%s.""" % fs.Storage.root_default(),
sub_title="subcommands",
sub_metavar="CMD")
# Common options.
#
# --dependencies (and --help and --version) are options rather than
# subcommands for consistency with other commands.
#
# These are also accepted *after* the subcommand, as it makes wrapping
# ch-image easier and possibly improve the UX. There are multiple ways to
# do this, though no tidy ones unfortunately. Here, we build up a
# dictionary of options we want, and pass it to both main and subcommand
# parsers; this works because both go into the same Namespace object. There
# are two quirks to be aware of:
#
# 1. We omit the common options from subcommand --help for clarity and
# because before the subcommand is preferred.
#
# 2. We suppress defaults in the subcommand [1]. Without this, the
# subcommand option value wins even it it’s the default. :P Currently,
# if specified in both places, the subcommand value wins and the
# before value is not considered at all, e.g. "ch-image -vv foo -v"
# gives verbosity 1, not 3. This oddity seemed acceptable.
#
# Alternate approaches include:
#
# * Set the main parser as the “parent” of the subcommand parser [2].
# This may be the documented approach? However, it adds all the
# subcommands to the subparser, which we don’t want. A workaround would
# be to create a *third* parser that’s the parent of both the main and
# subcommand parsers, but that seems like too much indirection to me.
#
# * A two-stage parse (parse_known_args(), then parse_args() to have the
# main parser look again) works [3], but is complicated and has some
# odd side effects e.g. multiple subcommands will be accepted.
#
# Each sub-list is a group of options. They key identifies the mutually
# exclustive group, or non-mutually exclusive if None.
#
# [1]: https://bugs.python.org/issue9351#msg373665
# [2]: https://docs.python.org/3/library/argparse.html#parents
# [3]: https://stackoverflow.com/a/54936198
common_opts = \
{ ("bucache", "build cache common options"): [
[["--cache"],
{ "action": "store_const",
"const": ch.Build_Mode.ENABLED,
"dest": "bucache",
"help": "enable build cache" }],
[["--no-cache"],
{ "action": "store_const",
"const": ch.Build_Mode.DISABLED,
"dest": "bucache",
"help": "disable build cache" }],
[["--rebuild"],
{ "action": "store_const",
"const": ch.Build_Mode.REBUILD,
"dest": "bucache",
"help": "force cache misses for non-FROM instructions" }] ],
(None, "misc common options"): [
[["-a", "--arch"],
{ "metavar": "ARCH",
"default": "host",
"help": "architecture for image registries (default: host)"}],
[["--always-download"],
{ "action": "store_true",
"help": "redownload any image files when pulling"}],
[["--auth"],
{ "action": "store_true",
"help": "authenticated registry access; implied by push" }],
[["--cache-large"],
{ "metavar": "SIZE",
"type": lambda s: ch.positive(s) * 2**20, # internal unit: bytes
"default": ch.positive(
os.environ.get("CH_IMAGE_CACHE_LARGE", 4)) * 2**20,
"help": "large file threshold in MiB (default: 4)" }],
[["--debug"],
{ "action": "store_true",
"help": "add short traceback to fatal error hints" }],
[["--dependencies"],
{ "action": misc.Dependencies,
"help": "print any missing dependencies and exit" }],
[["--no-lock"],
{ "action": "store_true",
"help": "allow concurrent storage directory access (risky!)" }],
[["--no-xattrs"],
{ "action": "store_true",
"help": "build cache ignores xattrs and ACLs"}],
[["--password-many"],
{ "action": "store_true",
"help": "re-prompt each time a registry password is needed" }],
[["--profile"],
{ "action": "store_true",
"help": "dump profile to “./profile.{p,txt}”" }],
[["-q", "--quiet"],
{ "action": "count",
"default": 0,
"help": "print less output (can be repeated)"}],
[["-s", "--storage"],
{ "metavar": "DIR",
"type": fs.Path,
"help": "set builder internal storage directory to DIR" }],
[["--tls-no-verify"],
{ "action": "store_true",
"help": "don’t verify registry certificates (dangerous!)" }],
[["-v", "--verbose"],
{ "action": "count",
"default": 0,
"help": "print extra chatter (can be repeated)" }],
[["--version"],
{ "action": misc.Version,
"help": "print version and exit" }] ] }
# Most, but not all, subcommands need to check dependencies before doing
# anything (the exceptions being basic information commands like
# storage-path). Similarly, only some need to initialize the storage
# directory. These dictionaries map the dispatch function to a boolean
# value saying whether to do those things.
dependencies_check = dict()
storage_init = dict()
# Helper function to set up a subparser. The star forces the latter two
# arguments to be called by keyword, for clarity.
def add_opts(p, dispatch, *, deps_check, stog_init, help_=False):
assert (not stog_init or deps_check) # can’t init storage w/o deps
if (dispatch is not None):
p.set_defaults(func=dispatch)
dependencies_check[dispatch] = deps_check
storage_init[dispatch] = stog_init
for ((name, title), group) in common_opts.items():
if (name is None):
p2 = p.add_argument_group(title=title)
else:
p2 = p.add_argument_group(title=title)
p2 = p2.add_mutually_exclusive_group()
for (args, kwargs) in group:
if (help_):
kwargs2 = kwargs
else:
kwargs2 = { **kwargs, "default": argparse.SUPPRESS }
p2.add_argument(*args, **kwargs2)
# main parser
add_opts(ap, None, deps_check=False, stog_init=False, help_=True)
# build
sp = ap.add_parser("build", "build image from Dockerfile")
add_opts(sp, build.main, deps_check=True, stog_init=True)
sp.add_argument("-b", "--bind", metavar="SRC[:DST]",
action="append", default=[],
help="mount SRC at guest DST (default: same as SRC)")
sp.add_argument("--build-arg", metavar="ARG[=VAL]",
action="append", default=[],
help="set build-time variable ARG to VAL, or $ARG if no VAL")
sp.add_argument("-f", "--file", metavar="DOCKERFILE",
help="Dockerfile to use (default: CONTEXT/Dockerfile)")
sp.add_argument("--force", metavar="MODE", nargs="?", default="seccomp",
type=ch.Force_Mode, const="seccomp",
help="inject unprivileged build workarounds")
sp.add_argument("--force-cmd", metavar="CMD,ARG1[,ARG2...]",
action="append", default=[],
help="command arg(s) to add under --force=seccomp")
sp.add_argument("-n", "--dry-run", action="store_true",
help="don’t execute instructions")
sp.add_argument("--parse-only", action="store_true",
help="stop after parsing the Dockerfile")
sp.add_argument("-t", "--tag", metavar="TAG",
help="name (tag) of image to create (default: inferred)")
sp.add_argument("context", metavar="CONTEXT",
help="context directory")
# build-cache
sp = ap.add_parser("build-cache", "print build cache information")
add_opts(sp, misc.build_cache, deps_check=True, stog_init=True)
sp.add_argument("--gc",
action="store_true",
help="run garbage collection first")
sp.add_argument("--reset",
action="store_true",
help="clear and re-initialize first")
sp.add_argument("--tree",
action="store_true",
help="print a text tree summary")
sp.add_argument("--dot", nargs="?", metavar="PATH", const="build-cache",
help="write DOT and PDF tree summaries")
# delete
sp = ap.add_parser("delete", "delete image from internal storage")
add_opts(sp, misc.delete, deps_check=True, stog_init=True)
sp.add_argument("image_ref", metavar="IMAGE_GLOB", help="image(s) to delete", nargs='+')
# gestalt (has sub-subcommands)
sp = ap.add_parser("gestalt", "query debugging configuration",
sub_title="subsubcommands", sub_metavar="CMD")
add_opts(sp, lambda x: False, deps_check=False, stog_init=False)
# bucache
tp = sp.add_parser("bucache", "exit successfully if build cache available")
add_opts(tp, misc.gestalt_bucache, deps_check=True, stog_init=False)
# bucache-dot
tp = sp.add_parser("bucache-dot", "exit success if can produce DOT trees")
add_opts(tp, misc.gestalt_bucache_dot, deps_check=True, stog_init=False)
# storage-path
tp = sp.add_parser("storage-path", "print storage directory path")
add_opts(tp, misc.gestalt_storage_path, deps_check=False, stog_init=False)
# python-path
tp = sp.add_parser("python-path", "print path to python interpreter in use")
add_opts(tp, misc.gestalt_python_path, deps_check=False, stog_init=False)
# logging
tp = sp.add_parser("logging", "print logging messages at all levels")
add_opts(tp, misc.gestalt_logging, deps_check=False, stog_init=False)
tp.add_argument("--fail", action="store_true",
help="also generate a fatal error")
# import
sp = ap.add_parser("import", "copy external image into storage")
add_opts(sp, misc.import_, deps_check=True, stog_init=True)
sp.add_argument("path", metavar="PATH",
help="directory or tarball to import")
sp.add_argument("image_ref", metavar="IMAGE_REF",
help="destination image name (tag)")
# list
sp = ap.add_parser("list", "print information about image(s)")
add_opts(sp, misc.list_, deps_check=True, stog_init=True)
sp.add_argument("-l", "--long", action="store_true",
help="use long listing format")
sp.add_argument("-u", "--undeletable", action="store_true",
help="list images that can be restored with “undelete”")
sp.add_argument("--undeleteable", action="store_true", dest="undeletable",
help=argparse.SUPPRESS)
sp.add_argument("image_ref", metavar="IMAGE_REF", nargs="?",
help="print details of this image only")
# pull
sp = ap.add_parser("pull",
"copy image from remote repository to local filesystem")
add_opts(sp, pull.main, deps_check=True, stog_init=True)
sp.add_argument("--last-layer", metavar="N", type=int,
help="stop after unpacking N layers")
sp.add_argument("--parse-only", action="store_true",
help="stop after parsing the image reference(s)")
sp.add_argument("source_ref", metavar="IMAGE_REF", help="image reference")
sp.add_argument("dest_ref", metavar="DEST_REF", nargs="?",
help="destination image reference (default: IMAGE_REF)")
# push
sp = ap.add_parser("push",
"copy image from local filesystem to remote repository")
add_opts(sp, push.main, deps_check=True, stog_init=True)
sp.add_argument("--image", metavar="DIR", type=fs.Path,
help="path to unpacked image (default: opaque path in storage dir)")
sp.add_argument("source_ref", metavar="IMAGE_REF", help="image to push")
sp.add_argument("dest_ref", metavar="DEST_REF", nargs="?",
help="destination image reference (default: IMAGE_REF)")
# reset
sp = ap.add_parser("reset", "delete everything in ch-image builder storage")
add_opts(sp, misc.reset, deps_check=True, stog_init=False)
# undelete
sp = ap.add_parser("undelete", "recover image from build cache")
add_opts(sp, misc.undelete, deps_check=True, stog_init=True)
sp.add_argument("image_ref", metavar="IMAGE_REF", help="image to recover")
# Monkey patch problematic characters out of stdout and stderr.
ch.monkey_write_streams()
# Parse it up!
if (len(sys.argv) < 2):
ap.print_help(file=sys.stderr)
ch.exit(1)
cli = ap.parse_args()
# Initialize.
ch.init(cli)
if (dependencies_check[cli.func]):
ch.dependencies_check()
if (storage_init[cli.func]):
ch.storage.init()
bu.init(cli)
# Dispatch.
ch.profile_start()
cli.func(cli)
ch.warnings_dump()
ch.exit(0)
## Bootstrap ##
if (__name__ == "__main__"):
try:
main()
except ch.Fatal_Error as x:
ch.warnings_dump()
ch.ERROR(*x.args, **x.kwargs)
ch.exit(1)