Exemplo n.º 1
0
 def manifest_load(self):
    """Parse the manifest file and set self.config_hash and
       self.layer_hashes."""
    def bad_key(key):
       ch.FATAL("manifest: %s: no key: %s" % (self.manifest_path, key))
    # read and parse the JSON
    fp = ch.open_(self.manifest_path, "rt", encoding="UTF-8")
    text = ch.ossafe(fp.read, "can't read: %s" % self.manifest_path)
    ch.ossafe(fp.close, "can't close: %s" % self.manifest_path)
    ch.DEBUG("manifest:\n%s" % text)
    try:
       manifest = json.loads(text)
    except json.JSONDecodeError as x:
       ch.FATAL("can't parse manifest file: %s:%d: %s"
                % (self.manifest_path, x.lineno, x.msg))
    # validate schema version
    try:
       version = manifest['schemaVersion']
    except KeyError:
       bad_key("schemaVersion")
    if (version not in {1,2}):
       ch.FATAL("unsupported manifest schema version: %s" % repr(version))
    # load config hash
    #
    # FIXME: Manifest version 1 does not list a config blob. It does have
    # things (plural) that look like a config at history/v1Compatibility as
    # an embedded JSON string :P but I haven't dug into it.
    if (version == 1):
       ch.WARNING("no config; manifest schema version 1")
       self.config_hash = None
    else:  # version == 2
       try:
          self.config_hash = ch.digest_trim(manifest["config"]["digest"])
       except KeyError:
          bad_key("config/digest")
    # load layer hashes
    if (version == 1):
       key1 = "fsLayers"
       key2 = "blobSum"
    else:  # version == 2
       key1 = "layers"
       key2 = "digest"
    if (key1 not in manifest):
       bad_key(key1)
    self.layer_hashes = list()
    for i in manifest[key1]:
       if (key2 not in i):
          bad_key("%s/%s" % (key1, key2))
       self.layer_hashes.append(ch.digest_trim(i[key2]))
    if (version == 1):
       self.layer_hashes.reverse()
Exemplo n.º 2
0
def main(cli_):

   # CLI namespace. :P
   global cli
   cli = cli_

   # Infer input file if needed.
   if (cli.file is None):
      cli.file = cli.context + "/Dockerfile"

   # Infer image name if needed.
   if (cli.tag is None):
      m = re.search(r"(([^/]+)/)?Dockerfile(\.(.+))?$",
                    os.path.abspath(cli.file))
      if (m is not None):
         if m.group(4):    # extension
            cli.tag = m.group(4)
         elif m.group(2):  # containing directory
            cli.tag = m.group(2)

   # Deal with build arguments.
   def build_arg_get(arg):
      kv = arg.split("=")
      if (len(kv) == 2):
         return kv
      else:
         v = os.getenv(kv[0])
         if (v is None):
            ch.FATAL("--build-arg: %s: no value and not in environment" % kv[0])
         return (kv[0], v)
   if (cli.build_arg is None):
      cli.build_arg = list()
   cli.build_arg = dict( build_arg_get(i) for i in cli.build_arg )

   # Finish CLI initialization.
   ch.DEBUG(cli)
   ch.dependencies_check()

   # Guess whether the context is a URL, and error out if so. This can be a
   # typical looking URL e.g. "https://..." or also something like
   # "[email protected]:...". The line noise in the second line of the regex is
   # to match this second form. Username and host characters from
   # https://tools.ietf.org/html/rfc3986.
   if (re.search(r"""  ^((git|git+ssh|http|https|ssh)://
                     | ^[\w.~%!$&'\(\)\*\+,;=-]+@[\w.~%!$&'\(\)\*\+,;=-]+:)""",
                 cli.context, re.VERBOSE) is not None):
      ch.FATAL("not yet supported: issue #773: URL context: %s" % cli.context)
   if (os.path.exists(cli.context + "/.dockerignore")):
      ch.WARNING("not yet supported, ignored: issue #777: .dockerignore file")

   # Set up build environment.
   global env
   env = Environment()

   # Read input file.
   if (cli.file == "-"):
      text = ch.ossafe(sys.stdin.read, "can't read stdin")
   else:
      fp = ch.open_(cli.file, "rt")
      text = ch.ossafe(fp.read, "can't read: %s" % cli.file)
      fp.close()

   # Parse it.
   parser = lark.Lark("?start: dockerfile\n" + ch.GRAMMAR,
                      parser="earley", propagate_positions=True)
   # Avoid Lark issue #237: lark.exceptions.UnexpectedEOF if the file does not
   # end in newline.
   text += "\n"
   try:
      tree = parser.parse(text)
   except lark.exceptions.UnexpectedInput as x:
      ch.DEBUG(x)  # noise about what was expected in the grammar
      ch.FATAL("can't parse: %s:%d,%d\n\n%s" % (cli.file, x.line, x.column, x.get_context(text, 39)))
   ch.DEBUG(tree.pretty())

   # Sometimes we exit after parsing.
   if (cli.parse_only):
      sys.exit(0)

   # Count the number of stages (i.e., FROM instructions)
   global image_ct
   image_ct = sum(1 for i in ch.tree_children(tree, "from_"))

   # Traverse the tree and do what it says.
   #
   # We don't actually care whether the tree is traversed breadth-first or
   # depth-first, but we *do* care that instruction nodes are visited in
   # order. Neither visit() nor visit_topdown() are documented as of
   # 2020-06-11 [1], but examining source code [2] shows that visit_topdown()
   # uses Tree.iter_trees_topdown(), which *is* documented to be in-order [3].
   #
   # This change seems to have been made in 0.8.6 (see PR #761); before then,
   # visit() was in order. Therefore, we call that instead, if visit_topdown()
   # is not present, to improve compatibility (see issue #792).
   #
   # [1]: https://lark-parser.readthedocs.io/en/latest/visitors/#visitors
   # [2]: https://github.com/lark-parser/lark/blob/445c8d4/lark/visitors.py#L211
   # [3]: https://lark-parser.readthedocs.io/en/latest/classes/#tree
   ml = Main_Loop()
   if (hasattr(ml, 'visit_topdown')):
      ml.visit_topdown(tree)
   else:
      ml.visit(tree)

   # Check that all build arguments were consumed.
   if (len(cli.build_arg) != 0):
      ch.FATAL("--build-arg: not consumed: " + " ".join(cli.build_arg.keys()))

   # Print summary & we're done.
   if (ml.instruction_ct == 0):
      ch.FATAL("no instructions found: %s" % cli.file)
   assert (image_i + 1 == image_ct)  # should have errored already if not
   ch.INFO("grown in %d instructions: %s"
           % (ml.instruction_ct, images[image_i]))
Exemplo n.º 3
0
 def execute_(self):
    env.env[self.key] = self.value
    with ch.open_(images[image_i].unpack_path + "/ch/environment", "wt") \
         as fp:
       for (k, v) in env.env.items():
          print("%s=%s" % (k, v), file=fp)