# 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 list of layers."""
# This is the main program to create a docker image. It expect to be run with:
# join_layers --output=output_file \
#             --layer=layer1 [--layer=layer2 ... --layer=layerN] \
#             --id=@identifier \
#             --name=myname --repository=repositoryName
# See the gflags declaration about the flags argument details.

import json
import os.path
import sys

from tools.build_defs.docker import utils
from tools.build_defs.pkg import archive
from third_party.py import gflags

gflags.DEFINE_string('output', None, 'The output file, mandatory')
gflags.MarkFlagAsRequired('output')

gflags.DEFINE_multistring('layer', [], 'The tar files for layers to join.')

gflags.DEFINE_multistring(
    'tags', [],
    'An associative list of fully qualified tag names and the layer they tag. '
    'e.g. ubuntu=deadbeef,gcr.io/blah/debian=baadf00d')

FLAGS = gflags.FLAGS


def _layer_filter(name):
  basename = os.path.basename(name)
  return basename not in ('manifest.json', 'top', 'repositories')


def _add_top(tar, repositories):
  # Don't add 'top' if there are multiple images in this bundle.
  if len(repositories) != 1:
    return

  # Walk the single-item dictionary, and if there is a single tag
  # for the single repository, then emit a 'top' file pointing to
  # the single image in this bundle.
  for (unused_x, tags) in repositories.iteritems():
    if len(tags) != 1:
      continue
    for (unused_y, layer_id) in tags.iteritems():
      tar.add_file('top', content=layer_id)


def create_image(output, layers, repositories=None):
  """Creates a Docker image from a list of layers.

  Args:
    output: the name of the docker image file to create.
    layers: the layers (tar files) to join to the image.
    repositories: the repositories two-level dictionary, which is keyed by
                  repo names at the top-level, and tag names at the second
                  level pointing to layer ids.
  """
  # Compute a map from layer tarball names to the tags that should apply to them
  layers_to_tag = {}
  for repo in repositories:
    tags = repositories[repo]
    for tag in tags:
      layer_name = tags[tag] + '/layer.tar'
      fq_name = '%s:%s' % (repo, tag)
      layer_tags = layers_to_tag.get(layer_name, [])
      layer_tags.append(fq_name)
      layers_to_tag[layer_name] = layer_tags

  manifests = []
  tar = archive.TarFileWriter(output)
  for layer in layers:
    tar.add_tar(layer, name_filter=_layer_filter)
    layer_manifests = utils.GetManifestFromTar(layer)

    # Augment each manifest with any tags that should apply to their top layer.
    for manifest in layer_manifests:
      top_layer = manifest['Layers'][-1]
      manifest['RepoTags'] = list(sorted(set(manifest['RepoTags'] +
                                             layers_to_tag.get(top_layer, []))))

    manifests += layer_manifests

  manifest_content = json.dumps(manifests, sort_keys=True)
  tar.add_file('manifest.json', content=manifest_content)

  # In addition to N layers of the form described above, there might be
  # 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',
  #     ...
  #   },
  #   ...
  # }
  # This is the exact structure we expect repositories to have.
  if repositories:
    # If the identifier is not provided, then the resulted layer will be
    # created without a 'top' file.  Docker doesn't needs that file nor
    # the repository to load the image and for intermediate layer,
    # docker_build store the name of the layer in a separate artifact so
    # this 'top' file is not needed.
    _add_top(tar, repositories)
    tar.add_file('repositories',
                 content=json.dumps(repositories, sort_keys=True))


def resolve_layer(identifier):
  if not identifier:
    # TODO(mattmoor): This should not happen.
    return None

  if not identifier.startswith('@'):
    return identifier

  with open(identifier[1:], 'r') as f:
    return f.read()


def main(unused_argv):
  repositories = {}
  for entry in FLAGS.tags:
    elts = entry.split('=')
    if len(elts) != 2:
      raise Exception('Expected associative list key=value, got: %s' % entry)
    (fq_tag, layer_id) = elts

    tag_parts = fq_tag.rsplit(':', 2)
    if len(tag_parts) != 2:
      raise Exception('Expected fully-qualified tag name (e.g. ubuntu:latest), '
                      'got: %s' % fq_tag)
    (repository, tag) = tag_parts

    others = repositories.get(repository, {})
    others[tag] = resolve_layer(layer_id)
    repositories[repository] = others

  create_image(FLAGS.output, FLAGS.layer, repositories)


if __name__ == '__main__':
  main(FLAGS(sys.argv))
