Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
cli.py 8.82 KiB
#!/usr/bin/env python3
import argparse
import os
import traceback
import sys

import hpccm
from hpccm.building_blocks import (
    packages,
    pip,
)
from hpccm.primitives import (
    baseimage,
    comment,
    raw,
)

from ogscm.version import __version__
from ogscm.app import builder
from ogscm.app.deployer import deployer
import shutil


def main():  # pragma: no cover

    recipe_args_parser = argparse.ArgumentParser(add_help=False)
    parser = argparse.ArgumentParser(add_help=False)
    recipe_args_parser.add_argument("recipe", nargs="+")
    parser.add_argument("recipe", nargs="+")

    # General args
    parser.add_argument(
        "--version",
        action="version",
        version="%(prog)s {version}".format(version=__version__),
    )
    parser.add_argument("--out", type=str, default="_out", help="Output directory")
    parser.add_argument(
        "--file", type=str, default="", help="Overwrite output recipe file name"
    )
    parser.add_argument(
        "--print",
        "-P",
        dest="print",
        action="store_true",
        help="Print the definition to stdout",
    )
    general_g = parser.add_argument_group("General image config")
    general_g.add_argument(
        "--format", type=str, choices=["docker", "singularity"], default="docker"
    )
    general_g.add_argument(
        "--base_image",
        type=str,
        default="ubuntu:20.04",
        help="The base image. (centos:8 is supported too)",
    )
    build_g = parser.add_argument_group("Image build options")
    build_g.add_argument(
        "--build",
        "-B",
        dest="build",
        action="store_true",
        help="Build the images from the definition files",
    )
    build_g.add_argument(
        "--build_args",
        type=str,
        default="",
        help="Arguments to the build command. Have to be "
        "quoted and **must** start with a space. E.g. "
        "--build_args ' --no-cache'",
    )
    build_g.add_argument(
        "--upload",
        "-U",
        dest="upload",
        action="store_true",
        help="Upload Docker image to registry",
    )
    build_g.add_argument(
        "--registry",
        type=str,
        default="registry.opengeosys.org/ogs/ogs",
        help="The docker registry the image is tagged and " "uploaded to.",
    )
    build_g.add_argument(
        "--tag",
        type=str,
        default="",
        help="The full docker image tag. Overwrites --registry.",
    )
    build_g.add_argument(
        "--convert",
        "-C",
        dest="convert",
        action="store_true",
        help="Convert Docker image to Singularity image",
    )
    build_g.add_argument(
        "--sif_file",
        type=str,
        default="",
        help="Overwrite output singularity image file name",
    )
    build_g.add_argument(
        "--convert-enroot",
        "-E",
        dest="convert_enroot",
        action="store_true",
        help="Convert Docker image to enroot image",
    )
    build_g.add_argument(
        "--enroot-bundle",
        dest="enroot_bundle",
        action="store_true",
        help="Convert enroot image to enroot bundle",
    )
    build_g.add_argument(
        "--enroot_file",
        type=str,
        default="",
        help="Overwrite output enroot image file name",
    )
    build_g.add_argument(
        "--force",
        dest="force",
        action="store_true",
        help="Forces overwriting of image files!",
    )
    build_g.add_argument(
        "--runtime-only",
        "-R",
        dest="runtime_only",
        action="store_true",
        help="Generate multi-stage Dockerfiles for small runtime " "images",
    )
    maint_g = parser.add_argument_group("Maintenance")
    maint_g.add_argument(
        "--clean",
        dest="cleanup",
        action="store_true",
        help="Cleans up generated files in default directories.",
    )
    deploy_g = parser.add_argument_group("Image deployment")
    deploy_g.add_argument(
        "--deploy",
        "-D",
        nargs="?",
        const="ALL",
        type=str,
        default="",
        help="Deploys to all configured hosts (in config/deploy_hosts.yml) with no additional arguments or to the specified host. Implies --build and --convert arguments.",
    )

    install_g = parser.add_argument_group("Packages to install")
    install_g.add_argument(
        "--pip",
        nargs="*",
        type=str,
        default=[],
        metavar="package",
        help="Install additional Python packages",
    )
    install_g.add_argument(
        "--packages",
        nargs="*",
        type=str,
        default=[],
        metavar="packages",
        help="Install additional OS packages",
    )

    args = parser.parse_known_args()[0]

    images_out_dir = os.path.abspath(f"{args.out}/images")
    if not os.path.exists(images_out_dir):
        os.makedirs(images_out_dir)

    Stage0 = hpccm.Stage()
    Stage0 += raw(docker="# syntax=docker/dockerfile:experimental")

    if args.runtime_only:
        Stage0.name = "build"
    Stage0 += baseimage(image=args.base_image, _as="build")

    Stage0 += comment(
        f"Generated with ogs-container-maker {__version__}", reformat=False
    )
    Stage0 += packages(ospackages=["wget", "tar", "curl", "make", "unzip"])

    # Prepare runtime stage
    Stage1 = hpccm.Stage()
    Stage1.baseimage(image=args.base_image)

    cwd = os.getcwd()
    img_file = ""
    out_dir = f"{args.out}/{args.format}"
    toolchain = None

    for recipe in recipe_args_parser.parse_known_args()[0].recipe:
        import importlib.resources as pkg_resources
        from ogscm import recipes

        # https://stackoverflow.com/a/1463370/80480
        ldict = {"filename": recipe}
        try:
            recipe_builtin = pkg_resources.read_text(recipes, recipe)
            exec(compile(recipe_builtin, recipe, "exec"), locals(), ldict)
        except Exception as err:
            error_class = err.__class__.__name__
            detail = err.args[0]
            cl, exc, tb = sys.exc_info()
            line_number = traceback.extract_tb(tb)[-1][1]
            print(f"{error_class} at line {line_number}: {detail}")
            if not os.path.exists(recipe):
                print(f"{recipe} does not exist!")
                exit(1)

            with open(recipe, "r") as reader:
                exec(compile(reader.read(), recipe, "exec"), locals(), ldict)
        if "out_dir" in ldict:
            out_dir = ldict["out_dir"]
        if "toolchain" in ldict:
            toolchain = ldict["toolchain"]
        if "img_file" not in ldict:
            print(f"img_file variable has to be set in {recipe}!")
            exit(1)
        img_file = ldict["img_file"]

    # Workaround to get the full help message
    help_parser = argparse.ArgumentParser(
        parents=[parser], formatter_class=argparse.ArgumentDefaultsHelpFormatter
    )
    help_parser.parse_args()

    # Finally parse
    args = parser.parse_args()

    ### container_info ###
    definition_file = "Dockerfile"
    if args.format == "singularity":
        definition_file = "Singularity.def"
    definition_file_path = os.path.join(out_dir, definition_file)
    if img_file[0] == "-":
        img_file = img_file[1:]
    if args.tag != "":
        tag = args.tag
    else:
        tag = f"{args.registry}/{img_file.lower()}:latest"
    # TODO:
    # context_path_size = len(self.ogsdir)
    # "{self.out_dir[context_path_size+1:]}/{self.definition_file}"
    ### end container_info ###

    if args.cleanup:
        shutil.rmtree(out_dir, ignore_errors=True)
        print("Cleaned up!")
        exit(0)
    if not os.path.exists(out_dir):
        os.makedirs(out_dir)  # For .scif files

    # General args
    if args.pip:
        Stage0 += pip(packages=args.pip, pip="pip3")
        Stage1 += pip(packages=args.pip, pip="pip3")

    if args.packages:
        Stage0 += packages(ospackages=args.packages)
        Stage1 += packages(ospackages=args.packages)

    # Create definition
    hpccm.config.set_container_format(args.format)

    stages_string = str(Stage0)

    if args.runtime_only:
        Stage1 += Stage0.runtime(exclude=["boost"])
        if args.compiler == "gcc" and args.compiler_version != None:
            Stage1 += packages(apt=["libstdc++6"])
        stages_string += "\n\n" + str(Stage1)

    # ---------------------------- recipe end -----------------------------
    with open(definition_file_path, "w") as f:
        print(stages_string, file=f)
    if args.print:
        print(stages_string)
    else:
        print(f"Created definition {os.path.abspath(definition_file_path)}")

    # Create image
    if not args.build:
        exit(0)

    b = builder(
        args,
        images_out_dir,
        img_file,
        definition_file_path,
        tag,
        cwd,
    )
    b.build()

    # Deploy image
    if not args.deploy:
        exit(0)

    deployer(args.deploy, cwd, b.image_file)


if __name__ == "__main__":  # pragma: no cover
    main()