#!/usr/bin/env python
# vim: set fileencoding=utf-8
# Pavel Odvody <podvody@redhat.com>
#
# HICA - Host integrated container applications
#
# MIT License (C) 2015

import json, os, sys, docker, argparse, imp, subprocess, shlex
from base.hica_base import *

# create docker client for inspecting images
dock = docker.Client()

_injectors_path = '/var/lib/docker-hica/injectors'
_selinux = '/usr/sbin/getenforce'
_injector_type = 'HicaInjector'
_injector_registry = {}

# adjust for container arguments passed after the -- delimeter
container_args = []
sliceat = 0
for i, arg in enumerate(sys.argv[1:]):
  if sliceat:
    container_args.append(arg)
  if arg == '--':
    sliceat = i + 1

if sliceat:
  sys.argv = sys.argv[:sliceat]

def _HicaBailIf(rc, *msg):
  if rc > 0:
    for m in msg:
      print(m)
    sys.exit(rc)

def HicaTestSingleGuest(injector, spec, image, config):
  run = ['docker', 'run', '--entrypoint', '/bin/sh'] + config + [image, '-c', spec]
  rc = subprocess.call(run)
  _HicaBailIf(rc, '*** Guest test failed for injector {0}!'.format(injector), 
                  '    ' + spec)

def HicaTestSingleHost(injector, spec):
  rc = subprocess.call(['/bin/sh', '-c', spec])
  _HicaBailIf(rc, '*** Host test failed for injector {0}!'.format(injector), 
                  '    ' + spec)

def HicaModuleFindInjectors(module):
  """ Find injectors in the given module

  :param module: Loaded module
  :type module: imp.Module
  """
  injectors = []
  for n in dir(module):
    attr = getattr(module, n)
    if hasattr(attr, '__base__') and attr.__base__.__name__ == _injector_type:
      injectors.append(attr)
  return injectors

def HicaLoadInjectors(path):
  """ Load all Python modules from `path`

  :param path: Relative path to module directory
  "type path: str
  """

  r = {}
  for inj in os.listdir(path):
    modname, suffix = inj.rsplit('.', 1)
    if suffix == 'py':
      fullpath = os.path.abspath(path)
      f, filename, description = imp.find_module(modname, [fullpath])
      m = imp.load_module(modname, open(filename, 'U'), filename, description)
      try:
        injs = HicaModuleFindInjectors(m)
        for i in injs:
          obj = i()
          r[obj.get_config_key()] = obj
      except AttributeError:
        print('Module {0} does not implement `register(context)`'.format(modname))
  return r

def HicaGetBaseArgparse():
  """ Provides with common `argparse` settings  """

  args = argparse.ArgumentParser()
  args.add_argument('hica_app_name')
  args.add_argument('named_action', default=None, nargs=argparse.REMAINDER)
  args.add_argument('--show-args', help='show possible arguments for the specified "hica_app_name"', default=False, action='store_true')
  args.add_argument('--test-injectors', help='executes injector tests for specified "hica_app_name"', default=False, action='store_true')
  args.add_argument('--verbose', help='print additional information', default=False, action='store_true')
  args.add_argument('--yes', help='bypass the capability review check', default=False, action='store_true')
  args.add_argument('--selinux-label', help='provide a confinement context', default='label:disable')
  args.add_argument('--user', help='user:group to run as ({0}:{0})'.format(str(os.getuid())), default='{0}:{0}'.format(str(os.getuid())))
  return args

def HicaGetImageName():
  """ Get image name so that we can fetch labels from it """

  args = HicaGetBaseArgparse()
  p, _ = args.parse_known_args()
  return p.hica_app_name

def HicaGetLabelsFromImage(image):
  """ Get labels from image as list

  :param image: Docker image
  :type image: dict
  """

  return [(k, v) for (k, v) in image['Config']['Labels'].iteritems()]

def HicaParseArguments(labels):
  """ Parse argument and optionally show usage and terminate with `0`
  return code

  :param labels: `Labels` read from the `Image`
  :type labels: list(str)
  """

  args = HicaGetBaseArgparse()
  # parse now to catch --show-args
  p, _ = args.parse_known_args()

  # top-level label cache
  seen = []

  # add needed argparsers
  for label in labels.items:
    if label[0] in _injector_registry:
      seen.append(label[0].rsplit('.', 1)[1])
      inj = _injector_registry[label[0]]
      inj.labels = labels
      for (arg, typ, defval) in inj.get_injected_args():
        if arg:
          args.add_argument(arg, default=defval)

  # now that we added the argparsers we can print usage and bail
  if p.show_args:
    args.print_usage()
    sys.exit(0)

  # test injectors
  if p.test_injectors:
    guest, host = [], []
    # collect tests
    for label in seen:
      gl = [(label, a, b) for (a, b) in labels.query(label, 'test.guest')]
      guest.extend(gl)
      host.extend(labels.query(label, 'test.host'))
    # run guest
    for (inj, lbl, spec) in guest:
      conf, iargs = [], []
      i = _injector_registry[HicaLabelStore.PREFIX + '.' + inj]
      for (_, t, v) in i.get_injected_args():
        iargs.append((t, v))
      i.inject_config(conf, iargs)
      HicaTestSingleGuest(lbl, spec, p.hica_app_name, conf)
    # run host
    for (lbl, spec) in host:
      HicaTestSingleHost(lbl, spec)
    sys.exit(0)

  return args.parse_args()

def HicaInjectConfiguration(labels, args, config):
  """ Inject configuration parameters for `labels` with values
  obtained from `args` into the `config` array

  :param labels: `Labels` read from the `Image`
  :type labels: list(str)
  :param args: Result of `parse_args` operation
  :type args: argparse.ArgumentParser
  :param config: Array where to inject configuration parameters
  :type config: list(str)
  """

  formatted = []
  actions = {}
  caps = []
  # map params from argparse back to injectors
  for label in labels.items:
    # command aliases are not handled via explicit injector
    if label[0] == 'io.hica.command_aliases':
      actions = json.loads(label[1])
    if label[0] in _injector_registry:
      inj = _injector_registry[label[0]]
      cfgs = []
      sub = labels.query_full(label[0], 'description')
      if sub:
        caps.append(sub[0][1])
      caps.append(inj.get_description())
      for (arg, typ, defval) in inj.get_injected_args():
        if arg:
          x = getattr(args, arg[2:].replace('-', '_'))
          cfgs.append((typ, x))
        else:
          cfgs.append((typ, defval))
      inj.inject_config(config, cfgs)

  # process named actions aka command aliases
  if args.named_action:
    action, aargs = args.named_action[0], args.named_action[1:]
    if action in actions:
      if len(aargs) > 0 and aargs[0] in ['--help', 'help']:
        print("{0} synopsis:\n {1}".format(action, actions[action]["synopsis"]))
        sys.exit(0)
      try:
        formatted = shlex.split(actions[action]["cmd"].format(*aargs))
      except IndexError:
        _HicaBailIf(1, '*** Action "{0}" expects parameters:\n    {1}'.format(action, actions[action]))

  instr = 'The container requests the following capabilities: \n - ' + '\n - '.join(caps) + '\nProceed? [y/Y/n]: '
  if args.yes or not caps or raw_input(instr) in ['y', 'Y']:
    return formatted, config
  else:
    _HicaBailIf(1, '*** Operation aborted!')

def HicaGetImages(images):
  """ Gets a list of all image names

  :param images: all docker images
  :type images: dict
  """
  tags = filter(lambda y: y != [u'<none>:<none>'], [x['RepoTags'] for x in images])
  return set([a for b in tags for a in b])

_injector_registry = HicaLoadInjectors(_injectors_path)

name = HicaGetImageName()
images = HicaGetImages(dock.images(all=True))

# search for the image, also try the default 'latest' tag
if name not in images and (name + ':latest') not in images:
  _HicaBailIf(1, '*** Image \'{0}\' not found locally, bailing!'.format(name))

image = dock.inspect_image(name)

labels = HicaGetLabelsFromImage(image)
ls = HicaLabelStore(labels)
parsed = HicaParseArguments(ls)

# base configuration
# run the container as current user so that file system rights are fine
# for bind mounted files
cfg = ['-i', '-u', parsed.user ]
# check if we have SELinux enabled
if os.path.exists(_selinux) and 'Enforcing' in subprocess.check_output([_selinux]):
   cfg += ['--security-opt', parsed.selinux_label]

aliased, config = HicaInjectConfiguration(ls, parsed, cfg)
runcmd = ['docker', 'run'] + config + [name] + container_args + aliased

if parsed.verbose:
  print('Executing: ' + ' '.join(runcmd))

p = subprocess.Popen(runcmd, stdin=sys.stdin)
out, err = p.communicate()
