#!/usr/bin/env python3

#   Copyright 2022 Red Hat, Inc.
#
#   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.

import argparse
from distutils import spawn
import logging
import os
import pwd
import subprocess
import sys


DESCRIPTION = "Build and execute Validations from a container."
EPILOG = "Example: ./validation --run --cmd run --validation check-ftype,512e"

LOCAL_USER = os.environ.get('SUDO_USER', os.environ.get('USER'))
VALIDATIONS_LOG_BASEDIR = os.path.expanduser(f'~{LOCAL_USER}/validations')
CONTAINER_INVENTORY_PATH = '/tmp/inventory.yaml'
COMMUNITY_VALIDATION_PATH = \
    os.path.expanduser(f'~{LOCAL_USER}/community-validations')

CONTAINERFILE_TEMPLATE = """
FROM %(image)s

LABEL name="VF dockerfile"

RUN groupadd -g %(gid)s -o %(user)s
RUN useradd -m -u %(uid)s -g %(gid)s -o -s /bin/bash %(user)s

RUN dnf install -y python3-pip gcc python3-devel libffi-devel jq openssh openssh-clients %(extra_pkgs)s

# Clone the Framework and common Validations
RUN python3 -m pip install validations-libs validations-common

# Clone user repository if provided
%(clone_user_repo)s
%(install_user_repo)s

#Setting up the default directory structure for both ansible,
#and the VF
RUN ln -s /usr/local/share/ansible  /usr/share/ansible

ENV ANSIBLE_HOST_KEY_CHECKING false
ENV ANSIBLE_RETRY_FILES_ENABLED false
ENV ANSIBLE_KEEP_REMOTE_FILES 1
ENV ANSIBLE_REMOTE_USER %(user)s
ENV ANSIBLE_PRIVATE_KEY_FILE %(user_dir)s/containerhost_private_key

USER %(user)s
%(entrypoint)s
"""


class Validation(argparse.ArgumentParser):
    """Validation client implementation class"""

    log = logging.getLogger(__name__ + ".Validation")

    def __init__(self, description=DESCRIPTION, epilog=EPILOG):
        """Init validation paser"""
        super(Validation, self).__init__(description=DESCRIPTION,
                                         epilog=EPILOG)

    def parser(self, parser):
        """Argument parser for validation"""
        user_entry = pwd.getpwuid(int(os.environ.get('SUDO_UID', os.getuid())))
        parser.add_argument('--run', '-R', action='store_true',
                            help=('Run Validation command. '
                                  'Defaults to False'))
        parser.add_argument('--interactive', '-i', action='store_true',
                            help=('Execute interactive Validation shell. '
                                  'Defaults to False'))
        parser.add_argument('--build', '-B', action='store_true',
                            help=('Build container even if it exists. '
                                  'Defaults to False'))
        parser.add_argument('--cmd', type=str, nargs=argparse.REMAINDER,
                            default=None,
                            help='Validation command you want to execute, '
                                 'use --help to get more information. '
                                 'Only available in non-interactive mode. ')
        parser.add_argument('--user', '-u', type=str, default='validation',
                            help=('Set user in the container. '))
        parser.add_argument('--user-home', type=str, default='/home/validation',
                            help=('User home path in the container. '
                                  'Example: --user-home /home/validation '))
        parser.add_argument('--uid', '-U', type=int, default=user_entry.pw_uid,
                            help=('User UID in container. '))
        parser.add_argument('--gid', '-G', type=int, default=user_entry.pw_gid,
                            help=('Group UID in container. '))
        parser.add_argument('--image', type=str, default='fedora:35',
                            help='Container base image. Defaults to fedora:35')
        parser.add_argument('--extra-pkgs', type=str, default='',
                            help=('Extra packages to install in the container.'
                                  'Comma or space separated list. '
                                  'Defaults to empty string.'))
        parser.add_argument('--volumes', '-v', type=str, action='append',
                            default=[],
                            help=('Volumes you want to add to the container. '
                                  'Can be provided multiple times. '
                                  'Defaults to []'))
        parser.add_argument('--keyfile', '-K', type=str,
                            default=os.path.join(os.path.expanduser('~'),
                                                 '.ssh/id_rsa'),
                            help=('Keyfile path to bind-mount in container. '))
        parser.add_argument('--engine', '-e', type=str, default='podman',
                            choices=['docker', 'podman'],
                            help='Container engine. Defaults to podman.')
        parser.add_argument('--validation-log-dir', '-l', type=str,
                            default=VALIDATIONS_LOG_BASEDIR,
                            help=('Path where the log files and artifacts '
                                  'will be located. '))
        parser.add_argument('--repository', '-r', type=str,
                            default=None,
                            help=('Remote repository to clone validations '
                                  'role from.'))
        parser.add_argument('--branch', '-b', type=str, default='master',
                            help=('Remote repository branch to clone '
                                  'validations from. Defaults to master'))

        parser.add_argument('--inventory', '-I', type=str,
                            default=None,
                            help=('Path of the Ansible inventory. '
                                  'It will be pulled to {} inside the '
                                  'container. '.format(
                                    CONTAINER_INVENTORY_PATH)))
        parser.add_argument('--debug', '-D', action='store_true',
                            help='Toggle debug mode. Defaults to False.')

        return parser.parse_args()

    def take_action(self, parsed_args):
        """Take validation action"""
        # Container params
        self.image = parsed_args.image
        self.extra_pkgs = parsed_args.extra_pkgs
        self.engine = parsed_args.engine
        self.validation_log_dir = parsed_args.validation_log_dir
        self.keyfile = parsed_args.keyfile
        self.interactive = parsed_args.interactive
        self.cmd = parsed_args.cmd
        self.user = parsed_args.user
        self.user_home = parsed_args.user_home
        self.uid = parsed_args.uid
        self.gid = parsed_args.gid
        self.repository = parsed_args.repository
        self.branch = parsed_args.branch
        self.debug = parsed_args.debug

        build = parsed_args.build
        run = parsed_args.run
        # Validation params
        self.inventory = parsed_args.inventory
        self.volumes = parsed_args.volumes

        if build:
            self.build()
        if run:
            self.run()

    def _print(self, string, debug=True):
        if self.debug:
            print(string)

    def _generate_containerfile(self):
        self._print('Generating "Containerfile"')
        clone_user_repo, install_user_repo, entrypoint = "", "", ""
        if self.repository:
            clone_user_repo = ("RUN git clone {} -b {} "
                               "{}/user_repo").format(self.repository,
                                                      self.branch,
                                                      self.user_home)
            install_user_repo = ("RUN cd {}/user_repo && \\"
                                 "python3 -m pip install .").format(
                                 self.user_home)
        if self.interactive:
            entrypoint = "ENTRYPOINT /usr/local/bin/validation"
        param = {'image': self.image, 'extra_pkgs': self.extra_pkgs,
                 'clone_user_repo': clone_user_repo,
                 'install_user_repo': install_user_repo,
                 'entrypoint': entrypoint,
                 'user': self.user, 'uid': self.uid, 'gid': self.gid,
                 'user_dir': self.user_home}
        with open('./Containerfile', 'w+') as containerfile:
            containerfile.write(CONTAINERFILE_TEMPLATE % param)

    def _check_container_cli(self, cli):
        if not spawn.find_executable(cli):
            raise RuntimeError(
                "The container cli {} doesn't exist on this host".format(cli))

    def _build_container(self):
        self._print('Building image')
        self._check_container_cli(self.engine)
        cmd = [
                self.engine,
                'build',
                '-t',
                'localhost/validation',
                '-f',
                'Containerfile',
                '.'
                ]
        if os.getuid() != 0:
            # build user needs to have sudo rights.
            cmd.insert(0, 'sudo')
        try:
            subprocess.check_call(cmd)
        except subprocess.CalledProcessError:
            print('An error occurred!')
            sys.exit(1)

    def _create_volume(self, path):
        try:
            self._print("Attempt to create {}.".format(path))
            os.mkdir(path)
        except (OSError, FileExistsError) as e:
            self._print(e)
            pass

    def _build_run_cmd(self):
        self._check_container_cli(self.engine)
        if self.interactive:
            container_args = '-ti'
        else:
            container_args = '--rm'
        cmd = [self.engine, 'run', container_args]
        # Keyfile
        cmd.append('-v%s:%s/containerhost_private_key:z' %
                   (self.keyfile, self.user_home))
        # log path
        self._create_volume(self.validation_log_dir)
        if os.path.isdir(os.path.abspath(self.validation_log_dir)):
            cmd.append('-v%s:%s/validations:z' %
                       (self.validation_log_dir, self.user_home))
        # community validation path
        self._create_volume(COMMUNITY_VALIDATION_PATH)
        if os.path.isdir(os.path.abspath(COMMUNITY_VALIDATION_PATH)):
            cmd.append('-v%s:%s/community-validations:z' %
                       (COMMUNITY_VALIDATION_PATH, self.user_home))
        # Volumes
        if self.volumes:
            self._print('Adding volumes:')
            for volume in self.volumes:
                self._print(volume)
                cmd.extend(['-v%s:z' % volume])
        # Inventory
        if self.inventory:
            if os.path.isfile(os.path.abspath(self.inventory)):
                cmd.append('-v%s:%s:z' % (
                    os.path.abspath(self.inventory),
                    CONTAINER_INVENTORY_PATH))
        # Map host network config
        cmd.append('--network=host')
        # Container name
        cmd.append('localhost/validation')
        # Validation binary
        cmd.append('validation')
        if not self.interactive and self.cmd:
            cmd.extend(self.cmd)
        return cmd

    def build(self):
        self._generate_containerfile()
        self._build_container()

    def run(self):
        self._print('Starting container')
        cmd = self._build_run_cmd()
        self._print('Running %s' % ' '.join(cmd))
        try:
            subprocess.check_call(cmd)
        except subprocess.CalledProcessError:
            print('An error occurred!')
            sys.exit(2)

if __name__ == "__main__":
    validation = Validation()
    args = validation.parser(validation)
    validation.take_action(args)
