|  | # Copyright 2016 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  | """This package manipulates OCI image configuration metadata.""" | 
|  | from collections import namedtuple | 
|  | import copy | 
|  | import json | 
|  | import os | 
|  | import os.path | 
|  | import sys | 
|  |  | 
|  | from tools.build_defs.docker import utils | 
|  | from third_party.py import gflags | 
|  |  | 
|  | gflags.DEFINE_string('base', None, 'The parent image') | 
|  |  | 
|  | gflags.DEFINE_string('output', None, 'The output file to generate') | 
|  | gflags.MarkFlagAsRequired('output') | 
|  |  | 
|  | gflags.DEFINE_multistring('layer', [], | 
|  | 'Layer sha256 hashes that make up this image') | 
|  |  | 
|  | gflags.DEFINE_list('entrypoint', None, | 
|  | 'Override the "Entrypoint" of the previous image') | 
|  |  | 
|  | gflags.DEFINE_list('command', None, 'Override the "Cmd" of the previous image') | 
|  |  | 
|  | gflags.DEFINE_string('user', None, 'The username to run commands under') | 
|  |  | 
|  | gflags.DEFINE_list('labels', None, 'Augment the "Label" of the previous image') | 
|  |  | 
|  | gflags.DEFINE_list('ports', None, | 
|  | 'Augment the "ExposedPorts" of the previous image') | 
|  |  | 
|  | gflags.DEFINE_list('volumes', None, | 
|  | 'Augment the "Volumes" of the previous image') | 
|  |  | 
|  | gflags.DEFINE_string('workdir', None, 'Set the working directory for the image') | 
|  |  | 
|  | gflags.DEFINE_list('env', None, 'Augment the "Env" of the previous image') | 
|  |  | 
|  | FLAGS = gflags.FLAGS | 
|  |  | 
|  | _ConfigOptionsT = namedtuple('ConfigOptionsT', ['layers', 'entrypoint', 'cmd', | 
|  | 'env', 'labels', 'ports', | 
|  | 'volumes', 'workdir', 'user']) | 
|  |  | 
|  |  | 
|  | class ConfigOptions(_ConfigOptionsT): | 
|  | """Docker image configuration options.""" | 
|  |  | 
|  | def __new__(cls, | 
|  | layers=None, | 
|  | entrypoint=None, | 
|  | cmd=None, | 
|  | user=None, | 
|  | labels=None, | 
|  | env=None, | 
|  | ports=None, | 
|  | volumes=None, | 
|  | workdir=None): | 
|  | """Constructor.""" | 
|  | return super(ConfigOptions, cls).__new__(cls, | 
|  | layers=layers, | 
|  | entrypoint=entrypoint, | 
|  | cmd=cmd, | 
|  | user=user, | 
|  | labels=labels, | 
|  | env=env, | 
|  | ports=ports, | 
|  | volumes=volumes, | 
|  | workdir=workdir) | 
|  |  | 
|  | _PROCESSOR_ARCHITECTURE = 'amd64' | 
|  |  | 
|  | _OPERATING_SYSTEM = 'linux' | 
|  |  | 
|  |  | 
|  | def Resolve(value, environment): | 
|  | """Resolves environment variables embedded in the given value.""" | 
|  | outer_env = os.environ | 
|  | try: | 
|  | os.environ = environment | 
|  | return os.path.expandvars(value) | 
|  | finally: | 
|  | os.environ = outer_env | 
|  |  | 
|  |  | 
|  | def DeepCopySkipNull(data): | 
|  | """Do a deep copy, skipping null entry.""" | 
|  | if isinstance(data, dict): | 
|  | return dict((DeepCopySkipNull(k), DeepCopySkipNull(v)) | 
|  | for k, v in data.iteritems() if v is not None) | 
|  | return copy.deepcopy(data) | 
|  |  | 
|  |  | 
|  | def KeyValueToDict(pair): | 
|  | """Converts an iterable object of key=value pairs to dictionary.""" | 
|  | d = dict() | 
|  | for kv in pair: | 
|  | (k, v) = kv.split('=', 1) | 
|  | d[k] = v | 
|  | return d | 
|  |  | 
|  |  | 
|  | def CreateImageConfig(data, options): | 
|  | """Create an image config possibly based on an existing one. | 
|  |  | 
|  | Args: | 
|  | data: A dict of Docker image config to base on top of. | 
|  | options: Options specific to this image which will be merged with any | 
|  | existing data | 
|  |  | 
|  | Returns: | 
|  | Image config for the new image | 
|  | """ | 
|  | defaults = DeepCopySkipNull(data) | 
|  |  | 
|  | # dont propagate non-spec keys | 
|  | output = dict() | 
|  | output['created'] = '0001-01-01T00:00:00Z' | 
|  | output['author'] = 'Bazel' | 
|  | output['architecture'] = _PROCESSOR_ARCHITECTURE | 
|  | output['os'] = _OPERATING_SYSTEM | 
|  |  | 
|  | output['config'] = defaults.get('config', {}) | 
|  |  | 
|  | if options.entrypoint: | 
|  | output['config']['Entrypoint'] = options.entrypoint | 
|  | if options.cmd: | 
|  | output['config']['Cmd'] = options.cmd | 
|  | if options.user: | 
|  | output['config']['User'] = options.user | 
|  |  | 
|  | def Dict2ConfigValue(d): | 
|  | return ['%s=%s' % (k, d[k]) for k in sorted(d.keys())] | 
|  |  | 
|  | if options.env: | 
|  | # Build a dictionary of existing environment variables (used by Resolve). | 
|  | environ_dict = KeyValueToDict(output['config'].get('Env', [])) | 
|  | # Merge in new environment variables, resolving references. | 
|  | for k, v in options.env.iteritems(): | 
|  | # Resolve handles scenarios like "PATH=$PATH:...". | 
|  | environ_dict[k] = Resolve(v, environ_dict) | 
|  | output['config']['Env'] = Dict2ConfigValue(environ_dict) | 
|  |  | 
|  | # TODO(babel-team) Label is currently docker specific | 
|  | if options.labels: | 
|  | label_dict = KeyValueToDict(output['config'].get('Label', [])) | 
|  | for k, v in options.labels.iteritems(): | 
|  | label_dict[k] = v | 
|  | output['config']['Label'] = Dict2ConfigValue(label_dict) | 
|  |  | 
|  | if options.ports: | 
|  | if 'ExposedPorts' not in output['config']: | 
|  | output['config']['ExposedPorts'] = {} | 
|  | for p in options.ports: | 
|  | if '/' in p: | 
|  | # The port spec has the form 80/tcp, 1234/udp | 
|  | # so we simply use it as the key. | 
|  | output['config']['ExposedPorts'][p] = {} | 
|  | else: | 
|  | # Assume tcp | 
|  | output['config']['ExposedPorts'][p + '/tcp'] = {} | 
|  |  | 
|  | if options.volumes: | 
|  | if 'Volumes' not in output['config']: | 
|  | output['config']['Volumes'] = {} | 
|  | for p in options.volumes: | 
|  | output['config']['Volumes'][p] = {} | 
|  |  | 
|  | if options.workdir: | 
|  | output['config']['WorkingDir'] = options.workdir | 
|  |  | 
|  | # diff_ids are ordered from bottom-most to top-most | 
|  | diff_ids = defaults.get('rootfs', {}).get('diff_ids', []) | 
|  | layers = options.layers if options.layers else [] | 
|  | diff_ids += ['sha256:%s' % l for l in layers] | 
|  | output['rootfs'] = { | 
|  | 'type': 'layers', | 
|  | 'diff_ids': diff_ids, | 
|  | } | 
|  |  | 
|  | # history is ordered from bottom-most layer to top-most layer | 
|  | history = defaults.get('history', []) | 
|  | # docker only allows the child to have one more history entry than the parent | 
|  | history += [{ | 
|  | 'created': '0001-01-01T00:00:00Z', | 
|  | 'created_by': 'bazel build ...', | 
|  | 'author': 'Bazel'}] | 
|  | output['history'] = history | 
|  |  | 
|  | return output | 
|  |  | 
|  |  | 
|  | def main(unused_argv): | 
|  | base_json = '{}' | 
|  | manifest = utils.GetLatestManifestFromTar(FLAGS.base) | 
|  | if manifest: | 
|  | config_file = manifest['Config'] | 
|  | base_json = utils.GetTarFile(FLAGS.base, config_file) | 
|  | data = json.loads(base_json) | 
|  |  | 
|  | layers = [] | 
|  | for layer in FLAGS.layer: | 
|  | layers.append(utils.ExtractValue(layer)) | 
|  |  | 
|  | labels = KeyValueToDict(FLAGS.labels) | 
|  | for label, value in labels.iteritems(): | 
|  | if value.startswith('@'): | 
|  | with open(value[1:], 'r') as f: | 
|  | labels[label] = f.read() | 
|  |  | 
|  | output = CreateImageConfig(data, | 
|  | ConfigOptions(layers=layers, | 
|  | entrypoint=FLAGS.entrypoint, | 
|  | cmd=FLAGS.command, | 
|  | user=FLAGS.user, | 
|  | labels=labels, | 
|  | env=KeyValueToDict(FLAGS.env), | 
|  | ports=FLAGS.ports, | 
|  | volumes=FLAGS.volumes, | 
|  | workdir=FLAGS.workdir)) | 
|  |  | 
|  | with open(FLAGS.output, 'w') as fp: | 
|  | json.dump(output, fp, sort_keys=True) | 
|  | fp.write('\n') | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | main(FLAGS(sys.argv)) |