Damien Martin-Guillerez | f88f4d8 | 2015-09-25 13:56:55 +0000 | [diff] [blame] | 1 | # Copyright 2015 The Bazel Authors. All rights reserved. |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | """This tool creates a docker image from a layer and the various metadata.""" |
| 15 | |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 16 | import json |
| 17 | import re |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 18 | import sys |
| 19 | import tarfile |
| 20 | |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 21 | from tools.build_defs.docker import utils |
Damien Martin-Guillerez | 80245bc | 2015-10-09 14:10:42 +0000 | [diff] [blame] | 22 | from tools.build_defs.pkg import archive |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 23 | from third_party.py import gflags |
| 24 | |
| 25 | # Hardcoded docker versions that we are claiming to be. |
| 26 | DATA_FORMAT_VERSION = '1.0' |
| 27 | |
| 28 | gflags.DEFINE_string( |
| 29 | 'output', None, |
| 30 | 'The output file, mandatory') |
| 31 | gflags.MarkFlagAsRequired('output') |
| 32 | |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 33 | gflags.DEFINE_multistring( |
| 34 | 'layer', [], |
| 35 | 'Layer tar files and their identifiers that make up this image') |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 36 | |
| 37 | gflags.DEFINE_string( |
| 38 | 'id', None, |
| 39 | 'The hex identifier of this image (hexstring or @filename), mandatory.') |
| 40 | gflags.MarkFlagAsRequired('id') |
| 41 | |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 42 | gflags.DEFINE_string('config', None, |
| 43 | 'The JSON configuration file for this image, mandatory.') |
| 44 | gflags.MarkFlagAsRequired('config') |
| 45 | |
| 46 | gflags.DEFINE_string('base', None, 'The base image file for this image.') |
| 47 | |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 48 | gflags.DEFINE_string( |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 49 | 'legacy_id', None, |
| 50 | 'The legacy hex identifier of this layer (hexstring or @filename).') |
| 51 | |
| 52 | gflags.DEFINE_string('metadata', None, |
| 53 | 'The legacy JSON metadata file for this layer.') |
| 54 | |
| 55 | gflags.DEFINE_string('legacy_base', None, |
| 56 | 'The legacy base image file for this image.') |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 57 | |
| 58 | gflags.DEFINE_string( |
| 59 | 'repository', None, |
| 60 | 'The name of the repository to add this image.') |
| 61 | |
| 62 | gflags.DEFINE_string( |
| 63 | 'name', None, |
| 64 | 'The symbolic name of this image.') |
| 65 | |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 66 | gflags.DEFINE_multistring('tag', None, |
| 67 | 'The repository tags to apply to the image') |
| 68 | |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 69 | FLAGS = gflags.FLAGS |
| 70 | |
| 71 | |
| 72 | def _base_name_filter(name): |
| 73 | """Do not add multiple times 'top' and 'repositories' when merging images.""" |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 74 | filter_names = ['top', 'repositories', 'manifest.json'] |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 75 | return all([not name.endswith(s) for s in filter_names]) |
| 76 | |
| 77 | |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 78 | def create_image(output, |
| 79 | identifier, |
| 80 | layers, |
| 81 | config, |
| 82 | tags=None, |
| 83 | base=None, |
| 84 | legacy_base=None, |
| 85 | metadata_id=None, |
| 86 | metadata=None, |
| 87 | name=None, |
| 88 | repository=None): |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 89 | """Creates a Docker image. |
| 90 | |
| 91 | Args: |
| 92 | output: the name of the docker image file to create. |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 93 | identifier: the identifier for this image (sha256 of the metadata). |
| 94 | layers: the layer content (a sha256 and a tar file). |
| 95 | config: the configuration file for the image. |
| 96 | tags: tags that apply to this image. |
| 97 | base: a base layer (optional) to build on top of. |
| 98 | legacy_base: a base layer (optional) to build on top of. |
| 99 | metadata_id: the identifier of the top layer for this image. |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 100 | metadata: the json metadata file for the top layer. |
| 101 | name: symbolic name for this docker image. |
| 102 | repository: repository name for this docker image. |
| 103 | """ |
| 104 | tar = archive.TarFileWriter(output) |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 105 | |
| 106 | # add the image config referenced by the Config section in the manifest |
| 107 | # the name can be anything but docker uses the format below |
| 108 | config_file_name = identifier + '.json' |
| 109 | tar.add_file(config_file_name, file_content=config) |
| 110 | |
| 111 | layer_file_names = [] |
| 112 | |
| 113 | if metadata_id: |
| 114 | # Write our id to 'top' as we are now the topmost layer. |
| 115 | tar.add_file('top', content=metadata_id) |
| 116 | |
| 117 | # Each layer is encoded as a directory in the larger tarball of the form: |
| 118 | # {id}\ |
| 119 | # layer.tar |
| 120 | # VERSION |
| 121 | # json |
| 122 | # Create the directory for us to now fill in. |
| 123 | tar.add_file(metadata_id + '/', tarfile.DIRTYPE) |
| 124 | # VERSION generally seems to contain 1.0, not entirely sure |
| 125 | # what the point of this is. |
| 126 | tar.add_file(metadata_id + '/VERSION', content=DATA_FORMAT_VERSION) |
| 127 | # Add the layer file |
| 128 | layer_file_name = metadata_id + '/layer.tar' |
| 129 | layer_file_names.append(layer_file_name) |
| 130 | tar.add_file(layer_file_name, file_content=layers[0]['layer']) |
| 131 | # Now the json metadata |
| 132 | tar.add_file(metadata_id + '/json', file_content=metadata) |
| 133 | |
| 134 | # Merge the base if any |
| 135 | if legacy_base: |
| 136 | tar.add_tar(legacy_base, name_filter=_base_name_filter) |
| 137 | else: |
| 138 | for layer in layers: |
| 139 | # layers can be called anything, so just name them by their sha256 |
| 140 | layer_file_name = identifier + '/' + layer['name'] + '.tar' |
| 141 | layer_file_names.append(layer_file_name) |
| 142 | tar.add_file(layer_file_name, file_content=layer['layer']) |
| 143 | |
| 144 | base_layer_file_names = [] |
| 145 | parent = None |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 146 | if base: |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 147 | latest_item = utils.GetLatestManifestFromTar(base) |
| 148 | if latest_item: |
| 149 | base_layer_file_names = latest_item.get('Layers', []) |
| 150 | config_file = latest_item['Config'] |
| 151 | parent_search = re.search('^(.+)\\.json$', config_file) |
| 152 | if parent_search: |
| 153 | parent = parent_search.group(1) |
| 154 | |
| 155 | manifest_item = { |
| 156 | 'Config': config_file_name, |
| 157 | 'Layers': base_layer_file_names + layer_file_names, |
| 158 | 'RepoTags': tags or [] |
| 159 | } |
| 160 | if parent: |
| 161 | manifest_item['Parent'] = 'sha256:' + parent |
| 162 | |
| 163 | manifest = [manifest_item] |
| 164 | |
| 165 | manifest_content = json.dumps(manifest, sort_keys=True) |
| 166 | tar.add_file('manifest.json', content=manifest_content) |
| 167 | |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 168 | # In addition to N layers of the form described above, there is |
| 169 | # a single file at the top of the image called repositories. |
| 170 | # This file contains a JSON blob of the form: |
| 171 | # { |
| 172 | # 'repo':{ |
| 173 | # 'tag-name': 'top-most layer hex', |
| 174 | # ... |
| 175 | # }, |
| 176 | # ... |
| 177 | # } |
| 178 | if repository: |
| 179 | tar.add_file('repositories', content='\n'.join([ |
| 180 | '{', |
| 181 | ' "%s": {' % repository, |
| 182 | ' "%s": "%s"' % (name, identifier), |
| 183 | ' }', |
| 184 | '}'])) |
| 185 | |
| 186 | |
| 187 | # Main program to create a docker image. It expect to be run with: |
| 188 | # create_image --output=output_file \ |
| 189 | # --id=@identifier \ |
| 190 | # [--base=base] \ |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 191 | # --layer=@identifier=layer.tar \ |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 192 | # --metadata=metadata.json \ |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 193 | # --name=myname --repository=repositoryName \ |
| 194 | # --tag=repo/image:tag |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 195 | # See the gflags declaration about the flags argument details. |
| 196 | def main(unused_argv): |
Sam Guymer | 6e3e48e | 2016-06-08 11:26:46 +0000 | [diff] [blame] | 197 | identifier = utils.ExtractValue(FLAGS.id) |
| 198 | legacy_id = utils.ExtractValue(FLAGS.legacy_id) |
| 199 | |
| 200 | layers = [] |
| 201 | for kv in FLAGS.layer: |
| 202 | (k, v) = kv.split('=', 1) |
| 203 | layers.append({ |
| 204 | 'name': utils.ExtractValue(k), |
| 205 | 'layer': v, |
| 206 | }) |
| 207 | |
| 208 | create_image(FLAGS.output, identifier, layers, FLAGS.config, FLAGS.tag, |
| 209 | FLAGS.base, FLAGS.legacy_base, legacy_id, FLAGS.metadata, |
Damien Martin-Guillerez | 5f25891 | 2015-07-27 08:27:53 +0000 | [diff] [blame] | 210 | FLAGS.name, FLAGS.repository) |
| 211 | |
| 212 | if __name__ == '__main__': |
| 213 | main(FLAGS(sys.argv)) |