-
Notifications
You must be signed in to change notification settings - Fork 60
implement ch-image modify
#1408
Changes from 8 commits
e8b04c9
2e1da45
83beda8
244d174
1ae7c94
580b6ec
e843576
d05460e
1df874f
50f917f
2f43bc9
491ce01
6f8014e
4996b4e
feee043
e641935
a0867d4
151592f
e0bc574
7d6f66e
2c8b2f7
731cfe8
fd45f64
05dac9b
00e2ec6
de7d63c
f7b87e5
4b84644
1afc853
c6ae8a9
4e61c06
9d74d19
579ec81
b37232b
3742a28
6e0ff69
26ce9e6
dbbf74d
8cb7975
b859089
54c2ee8
bb0283e
6bc35a9
8a97d8d
5be8c3e
c53a820
1dcbb4d
3314546
6ec4b26
b6da358
2b96ecc
ed46d89
fc5c89a
a9b1e23
dc5ed4d
1593009
7c320e7
d3b06fb
ce2a620
7f17900
8d0befc
1b65df9
d4f90f3
09f78f2
681a39c
2cd6149
20e2d91
32efbe4
f4b1ba7
62fe3e2
621b19a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ import charliecloud as ch | |
import build | ||
import build_cache as bu | ||
import filesystem as fs | ||
import image as im | ||
import misc | ||
import pull | ||
import push | ||
|
@@ -278,6 +279,14 @@ def main(): | |
sp.add_argument("image_ref", metavar="IMAGE_REF", nargs="?", | ||
help="print details of this image only") | ||
|
||
# modify | ||
sp = ap.add_parser("modify", "foo") | ||
add_opts(sp, build.modify, deps_check=True, stog_init=True) | ||
sp.add_argument("-c", metavar="CMD", action="append", default=[], nargs="*", help="foo") | ||
sp.add_argument("-o", "--out", metavar="out_image", help="foo", required=True) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this is required, should it be a second argument rather than an option? That said, it would be better UX if it’s not required. |
||
sp.add_argument("-S", "--shell", metavar="shell", help="foo") | ||
sp.add_argument("image_ref", metavar="IMAGE_REF", help="image to modify") | ||
|
||
# pull | ||
sp = ap.add_parser("pull", | ||
"copy image from remote repository to local filesystem") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,7 @@ | |
import os.path | ||
import re | ||
import shutil | ||
import subprocess | ||
import sys | ||
|
||
import charliecloud as ch | ||
|
@@ -57,6 +58,10 @@ class Environment: | |
of this is just passed through from the image metadata.""" | ||
|
||
|
||
# Class responsible for traversing the parse tree generated by lark. “Main_Loop” | ||
# visits each node in the parse tree and calls its “__default__” method to | ||
# figure out what to do with the node. This behavior is defined by the parent | ||
# class, “lark.Visitor”, documented here: https://lark-parser.readthedocs.io/en/latest/visitors.html | ||
lucaudill marked this conversation as resolved.
Show resolved
Hide resolved
|
||
class Main_Loop(lark.Visitor): | ||
|
||
__slots__ = ("instruction_total_ct", | ||
|
@@ -69,15 +74,28 @@ def __init__(self, *args, **kwargs): | |
self.instruction_total_ct = 0 | ||
super().__init__(*args, **kwargs) | ||
|
||
# The main argument of the “__default__” method is “tree”, which is really | ||
# just the current node being visited (the node is called “tree” because it | ||
# represents the root node of the current subtree). The “tree.data” attribute | ||
# gives the “name of the rule or alias” represented by the node. When a | ||
# “lark.Visitor” instance visits a parse tree node, it checks the node’s | ||
# “data” attribute and tries to call its own attribute (e.g. method) of the | ||
# same name. If no such attribute exists, it calls “__default__”. Note that | ||
# in this class, we don’t define any attributes corresponding to the | ||
# Dockerfile instructions that we want to execute, so “__default__” is always | ||
# called when visiting a node. We rely on instruction classes to execute the | ||
# instructions, rather than attributes of this class. | ||
lucaudill marked this conversation as resolved.
Show resolved
Hide resolved
|
||
def __default__(self, tree): | ||
class_ = tree.data.title() + "_G" | ||
ch.ILLERI(tree.data.title()) | ||
if (class_ in globals()): | ||
inst = globals()[class_](tree) | ||
if (self.instruction_total_ct == 0): | ||
if (not (isinstance(inst, Directive_G) | ||
or isinstance(inst, From__G) | ||
or isinstance(inst, Instruction_No_Image))): | ||
ch.FATAL("first instruction must be ARG or FROM") | ||
ch.ILLERI("PREV: %s" % self.inst_prev) | ||
inst.init(self.inst_prev) | ||
# The three announce_maybe() calls are clunky but I couldn’t figure | ||
# out how to avoid the repeats. | ||
|
@@ -287,6 +305,129 @@ def unescape(sl): | |
assert (len(sl) >= 2 and sl[0] == '"' and sl[-1] == '"' and sl[-2:] != '\\"') | ||
return ast.literal_eval(sl) | ||
|
||
def modify(cli_): | ||
global cli | ||
cli = cli_ | ||
|
||
cli.parse_only = False | ||
cli.force = ch.Force_Mode.SECCOMP | ||
lucaudill marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cli.force_cmd = force.FORCE_CMD_DEFAULT | ||
cli.bind = [] | ||
reidpr marked this conversation as resolved.
Show resolved
Hide resolved
lucaudill marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# FIXME: This is super kludgey | ||
cli.tag = cli.out | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can tell |
||
|
||
print(cli.image_ref) | ||
ch.ILLERI(cli.c) | ||
ch.ILLERI(type(cli.c)) | ||
commands = [] | ||
# “Flatten” commands array | ||
for c in cli.c: | ||
commands += c | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The need for this will go away if you remove |
||
src_image = im.Image(im.Reference(cli.image_ref)) | ||
out_image = cli.out | ||
if (not src_image.unpack_exist_p): | ||
ch.FATAL("not in storage: %s" % src_image.ref) | ||
if (out_image == str(src_image.ref)): | ||
ch.FATAL("output image must have different name from source (%s)" % src_image.ref) | ||
if (cli.shell is not None): | ||
shell = cli.shell | ||
else: | ||
shell = "/bin/sh" | ||
if not sys.stdin.isatty(): | ||
# https://stackoverflow.com/a/17735803 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This idiom is well enough known that you don't need a comment. |
||
# commands from stdin | ||
for line in sys.stdin: | ||
# execute each line (analogous to RUN) | ||
commands.append(line) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won’t work due to the shell’s complicated rules about line breaks. I’d treat the input as an opaque blob to pass on to the shell’s stdin. |
||
if (commands != []): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if the |
||
tree = modify_tree_make(src_image.ref, commands) | ||
|
||
# FIXME: Be more DRY in this section | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah that's probably essential for merging |
||
|
||
# Count the number of stages (i.e., FROM instructions) | ||
global image_ct | ||
image_ct = sum(1 for i in tree.children_("from_")) | ||
lucaudill marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
ml = Main_Loop() | ||
if (hasattr(ml, 'visit_topdown')): | ||
ml.visit_topdown(tree) | ||
else: | ||
ml.visit(tree) | ||
if (ml.instruction_total_ct > 0): | ||
if (ml.miss_ct == 0): | ||
ml.inst_prev.checkout() | ||
ml.inst_prev.ready() | ||
|
||
else: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should label the branches on which of the three modes we’re in. |
||
# Make sure that shell exists. | ||
#try: | ||
# subprocess.run([ch.CH_BIN + "/ch-run", str(src_image.ref), "--", shell], capture_output=True).check_returncode() | ||
#except subprocess.CalledProcessError as x: | ||
# #print(x.__dict__) | ||
# if ("%s: No such file or directory" % shell in str(x.stderr)): | ||
# ch.FATAL("invalid shell: %s" % shell) | ||
|
||
out_image = im.Image(im.Reference(out_image)) | ||
# Do something similar to “ch-image import” | ||
out_image.unpack_clear() | ||
out_image.copy_unpacked(src_image) | ||
subprocess.run([ch.CH_BIN + "/ch-run", "--unsafe", "-w", | ||
str(out_image.ref), "--", shell]) | ||
# FIXME: metadata history stuff? See misc.import_. | ||
#bu.cache.rollback(src_image.unpack_path) | ||
|
||
def modify_tree_make(src_img, cmds): | ||
"""Function that manually constructs a parse tree corresponding to a set of | ||
lucaudill marked this conversation as resolved.
Show resolved
Hide resolved
|
||
“ch-image modify” commands, as though the commands had been specified in a | ||
Dockerfile. Note that because “ch-image modify” simply executes one or | ||
more commands inside a container, the only Dockerfile instructions we need | ||
to consider are “FROM” and “RUN”. This results in conveniently simple | ||
parse trees of the form: | ||
|
||
start | ||
dockerfile | ||
from_ | ||
image_ref | ||
IMAGE_REF <image_name> | ||
run | ||
run_shell | ||
LINE_CHUNK <command_1> | ||
[...] | ||
run | ||
run_shell | ||
LINE_CHUNK <command_N> | ||
lucaudill marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
e.g. for the command line | ||
|
||
$ ch-image modify -o foo2 -c 'echo foo' -c 'echo bar' -- foo | ||
|
||
this function produces the following parse tree | ||
|
||
start | ||
dockerfile | ||
from_ | ||
image_ref | ||
IMAGE_REF foo | ||
run | ||
run_shell | ||
LINE_CHUNK echo foo | ||
run | ||
run_shell | ||
LINE_CHUNK echo bar | ||
""" | ||
# Children of dockerfile tree | ||
df_children = [] | ||
# Metadata attribute. We use this attribute in the “_pretty” method for our | ||
# “Tree” class. Constructing a tree without specifying a “Meta” instance that | ||
# has been given a “line” value will result in the attribute not being present, | ||
# which causes an error when we try to access that attribute. Here we give the | ||
# attribute a debug value of -1 to avoid said errors. | ||
meta = lark.tree.Meta() | ||
meta.line = -1 | ||
df_children.append(im.Tree(lark.Token('RULE', 'from_'), [im.Tree(lark.Token('RULE', 'image_ref'),[lark.Token('IMAGE_REF', src_img.name)], meta)], meta)) | ||
for cmd in cmds: | ||
df_children.append(im.Tree(lark.Token('RULE', 'run'), [im.Tree(lark.Token('RULE', 'run_shell'),[lark.Token('LINE_CHUNK', cmd)], meta)],meta)) | ||
return im.Tree(lark.Token('RULE', 'start'), [im.Tree(lark.Token('RULE','dockerfile'), df_children)], meta) | ||
|
||
## Supporting classes ## | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you probably want
nargs=1
(the default?) and just repeat the option for multiple values (action="append"
does this).nargs="+"
andnargs="*"
can cause problems though I don't recall the details.