blob: a5faf9f51626c1bef89f15fc87cbde7c8c09f7cf [file] [log] [blame]
# 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))