# Copyright 2015 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 tool creates a docker image from a layer and the various metadata.""" import json import re import sys import tarfile from tools.build_defs.docker import utils from tools.build_defs.pkg import archive from third_party.py import gflags # Hardcoded docker versions that we are claiming to be. DATA_FORMAT_VERSION = '1.0' gflags.DEFINE_string( 'output', None, 'The output file, mandatory') gflags.MarkFlagAsRequired('output') gflags.DEFINE_multistring( 'layer', [], 'Layer tar files and their identifiers that make up this image') gflags.DEFINE_string( 'id', None, 'The hex identifier of this image (hexstring or @filename), mandatory.') gflags.MarkFlagAsRequired('id') gflags.DEFINE_string('config', None, 'The JSON configuration file for this image, mandatory.') gflags.MarkFlagAsRequired('config') gflags.DEFINE_string('base', None, 'The base image file for this image.') gflags.DEFINE_string( 'legacy_id', None, 'The legacy hex identifier of this layer (hexstring or @filename).') gflags.DEFINE_string('metadata', None, 'The legacy JSON metadata file for this layer.') gflags.DEFINE_string('legacy_base', None, 'The legacy base image file for this image.') gflags.DEFINE_string( 'repository', None, 'The name of the repository to add this image.') gflags.DEFINE_string( 'name', None, 'The symbolic name of this image.') gflags.DEFINE_multistring('tag', None, 'The repository tags to apply to the image') FLAGS = gflags.FLAGS def _base_name_filter(name): """Do not add multiple times 'top' and 'repositories' when merging images.""" filter_names = ['top', 'repositories', 'manifest.json'] return all([not name.endswith(s) for s in filter_names]) def _create_image(tar, identifier, layers, config, tags=None, base=None, legacy_base=None, metadata_id=None, metadata=None, name=None, repository=None): """Creates a Docker image. Args: tar: archive.TarFileWriter object for the docker image file to create. identifier: the identifier for this image (sha256 of the metadata). layers: the layer content (a sha256 and a tar file). config: the configuration file for the image. tags: tags that apply to this image. base: a base layer (optional) to build on top of. legacy_base: a base layer (optional) to build on top of. metadata_id: the identifier of the top layer for this image. metadata: the json metadata file for the top layer. name: symbolic name for this docker image. repository: repository name for this docker image. """ # add the image config referenced by the Config section in the manifest # the name can be anything but docker uses the format below config_file_name = identifier + '.json' tar.add_file(config_file_name, file_content=config) layer_file_names = [] if metadata_id: # Write our id to 'top' as we are now the topmost layer. tar.add_file('top', content=metadata_id) # Each layer is encoded as a directory in the larger tarball of the form: # {id}\ # layer.tar # VERSION # json # Create the directory for us to now fill in. tar.add_file(metadata_id + '/', tarfile.DIRTYPE) # VERSION generally seems to contain 1.0, not entirely sure # what the point of this is. tar.add_file(metadata_id + '/VERSION', content=DATA_FORMAT_VERSION) # Add the layer file layer_file_name = metadata_id + '/layer.tar' layer_file_names.append(layer_file_name) tar.add_file(layer_file_name, file_content=layers[0]['layer']) # Now the json metadata tar.add_file(metadata_id + '/json', file_content=metadata) # Merge the base if any if legacy_base: tar.add_tar(legacy_base, name_filter=_base_name_filter) else: for layer in layers: # layers can be called anything, so just name them by their sha256 layer_file_name = identifier + '/' + layer['name'] + '.tar' layer_file_names.append(layer_file_name) tar.add_file(layer_file_name, file_content=layer['layer']) base_layer_file_names = [] parent = None if base: latest_item = utils.GetLatestManifestFromTar(base) if latest_item: base_layer_file_names = latest_item.get('Layers', []) config_file = latest_item['Config'] parent_search = re.search('^(.+)\\.json$', config_file) if parent_search: parent = parent_search.group(1) manifest_item = { 'Config': config_file_name, 'Layers': base_layer_file_names + layer_file_names, 'RepoTags': tags or [] } if parent: manifest_item['Parent'] = 'sha256:' + parent manifest = [manifest_item] manifest_content = json.dumps(manifest, sort_keys=True) tar.add_file('manifest.json', content=manifest_content) # In addition to N layers of the form described above, there is # a single file at the top of the image called repositories. # This file contains a JSON blob of the form: # { # 'repo':{ # 'tag-name': 'top-most layer hex', # ... # }, # ... # } if repository: tar.add_file('repositories', content='\n'.join([ '{', ' "%s": {' % repository, ' "%s": "%s"' % (name, identifier), ' }', '}'])) def create_image(output, identifier, layers, config, tags=None, base=None, legacy_base=None, metadata_id=None, metadata=None, name=None, repository=None): """Creates a Docker image. Args: output: the name of the docker image file to create. identifier: the identifier for this image (sha256 of the metadata). layers: the layer content (a sha256 and a tar file). config: the configuration file for the image. tags: tags that apply to this image. base: a base layer (optional) to build on top of. legacy_base: a base layer (optional) to build on top of. metadata_id: the identifier of the top layer for this image. metadata: the json metadata file for the top layer. name: symbolic name for this docker image. repository: repository name for this docker image. """ with archive.TarFileWriter(output) as tar: _create_image(tar, identifier, layers, config, tags, base, legacy_base, metadata_id, metadata, name, repository) # Main program to create a docker image. It expect to be run with: # create_image --output=output_file \ # --id=@identifier \ # [--base=base] \ # --layer=@identifier=layer.tar \ # --metadata=metadata.json \ # --name=myname --repository=repositoryName \ # --tag=repo/image:tag # See the gflags declaration about the flags argument details. def main(unused_argv): identifier = utils.ExtractValue(FLAGS.id) legacy_id = utils.ExtractValue(FLAGS.legacy_id) layers = [] for kv in FLAGS.layer: (k, v) = kv.split('=', 1) layers.append({ 'name': utils.ExtractValue(k), 'layer': v, }) create_image(FLAGS.output, identifier, layers, FLAGS.config, FLAGS.tag, FLAGS.base, FLAGS.legacy_base, legacy_id, FLAGS.metadata, FLAGS.name, FLAGS.repository) if __name__ == '__main__': main(FLAGS(sys.argv))