| # 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)) |