From 39f4c49bd24f4ec14e1083d472c98c834140e3ab Mon Sep 17 00:00:00 2001 From: Klaus Reuter <khr@mpcdf.mpg.de> Date: Wed, 25 Oct 2023 16:58:38 +0200 Subject: [PATCH] implement dryrun functionality, add tests to CI --- .gitlab-ci.yml | 11 +++ condainer/condainer.py | 149 +++++++++++++++++++++++++++++------------ condainer/main.py | 4 +- 3 files changed, 122 insertions(+), 42 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7253f7e..99fbea2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,17 @@ basic: - module load anaconda/3/2023.03 script: - pip3 install --user -e . + - mkdir test + - pushd test - ~/.local/bin/cnd --help + - ~/.local/bin/cnd --dryrun init + - touch Miniforge3-Linux-x86_64.sh + - ~/.local/bin/cnd init + - ~/.local/bin/cnd --dryrun build + - ~/.local/bin/cnd --dryrun --steps 1,2,3,7 build + - ~/.local/bin/cnd --dryrun mount + - ~/.local/bin/cnd --dryrun umount + - ~/.local/bin/cnd --dryrun exec -- python3 + - popd only: - master diff --git a/condainer/condainer.py b/condainer/condainer.py index a1723d6..ea4828b 100644 --- a/condainer/condainer.py +++ b/condainer/condainer.py @@ -158,13 +158,16 @@ def create_base_environment(cfg): """ conda_installer = get_installer_path(cfg) env_directory = get_env_directory(cfg) - cmd = f"bash {conda_installer} -b -f -p {env_directory}".split() + cmd = f"/bin/bash {conda_installer} -b -f -p {env_directory}".split() env = copy.deepcopy(os.environ) if "PYTHONPATH" in env: del env["PYTHONPATH"] - proc = subprocess.Popen(cmd, shell=False, env=env) - proc.communicate() - assert(proc.returncode == 0) + if cfg.get("dryrun"): + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False, env=env) + proc.communicate() + assert(proc.returncode == 0) def create_condainer_environment(cfg): @@ -177,9 +180,12 @@ def create_condainer_environment(cfg): env = copy.deepcopy(os.environ) if "PYTHONPATH" in env: del env["PYTHONPATH"] - proc = subprocess.Popen(cmd, shell=False, env=env) - proc.communicate() - assert(proc.returncode == 0) + if cfg.get("dryrun"): + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False, env=env) + proc.communicate() + assert(proc.returncode == 0) def pip_condainer_environment(cfg): @@ -193,9 +199,15 @@ def pip_condainer_environment(cfg): env = copy.deepcopy(os.environ) if "PYTHONPATH" in env: del env["PYTHONPATH"] - proc = subprocess.Popen(cmd, shell=False, env=env) - proc.communicate() - assert(proc.returncode == 0) + if cfg.get("dryrun"): + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False, env=env) + proc.communicate() + assert(proc.returncode == 0) + else: + if not cfg.get("quiet"): + print(f"{requirements_txt} not found, skipping pip") def clean_environment(cfg): @@ -207,9 +219,12 @@ def clean_environment(cfg): env = copy.deepcopy(os.environ) if "PYTHONPATH" in env: del env["PYTHONPATH"] - proc = subprocess.Popen(cmd, shell=False, env=env) - proc.communicate() - assert(proc.returncode == 0) + if cfg.get("dryrun"): + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False, env=env) + proc.communicate() + assert(proc.returncode == 0) def compress_environment(cfg): @@ -218,9 +233,12 @@ def compress_environment(cfg): env_directory = get_env_directory(cfg) squashfs_image = get_image_filename(cfg) cmd = f"mksquashfs {env_directory}/ {squashfs_image} -noappend".split() - proc = subprocess.Popen(cmd, shell=False) - proc.communicate() - assert(proc.returncode == 0) + if cfg.get("dryrun"): + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False) + proc.communicate() + assert(proc.returncode == 0) def run_cmd(args, cwd): @@ -231,8 +249,11 @@ def run_cmd(args, cwd): bin_directory = os.path.join(env_directory, 'envs', 'condainer', 'bin') env = copy.deepcopy(os.environ) env['PATH'] = bin_directory + ':' + env['PATH'] - proc = subprocess.Popen(args.command, cwd=cwd, env=env, shell=False) - proc.communicate() + if args.dryrun: + print(f"dryrun: {bin_directory}:{args.command}") + else: + proc = subprocess.Popen(args.command, cwd=cwd, env=env, shell=False) + proc.communicate() # --- condainer entry point functions below --- @@ -257,7 +278,8 @@ def init(args): condainer_yml = "condainer.yml" if not os.path.isfile(condainer_yml): - write_cfg(cfg) + if not args.dryrun: + write_cfg(cfg) else: print(f"STOP. Found existing file {condainer_yml}, please run `init` from an empty directory.") sys.exit(1) @@ -268,24 +290,30 @@ def init(args): if not args.quiet: print("Downloading conda installer ...") cmd = f"curl -JLO {cfg['installer_url']}".split() - proc = subprocess.Popen(cmd, shell=False) - proc.communicate() - assert(proc.returncode == 0) + if args.dryrun: + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False) + proc.communicate() + assert(proc.returncode == 0) else: if not args.quiet: - print(f"Found existing installer {conda_installer}, skipping download.") + print(f"found existing installer {conda_installer}, skipping download") else: if not args.quiet: - print(f"Using installer {cfg['installer_url']}") + print(f"using installer {cfg['installer_url']}") assert(os.path.isfile(cfg['installer_url'])) - write_example_environment_yml() + if not args.dryrun: + write_example_environment_yml() def build(args): """Create conda environment and create compressed squashfs image from it. """ cfg = get_cfg() + cfg["quiet"] = args.quiet + cfg["dryrun"] = args.dryrun squashfs_image = get_image_filename(cfg) env_directory = get_env_directory(cfg) if os.path.isfile(squashfs_image): @@ -296,18 +324,50 @@ def build(args): sys.exit(1) else: try: - os.makedirs(env_directory, exist_ok=True, mode=0o700) - create_base_environment(cfg) - create_condainer_environment(cfg) - pip_condainer_environment(cfg) - clean_environment(cfg) - compress_environment(cfg) - write_activate_script(cfg) - write_deactivate_script(cfg) + steps = {int(i) for i in args.steps.split(',')} + if not args.quiet: + print(termcol.BOLD+"Starting Condainer build process ..."+termcol.ENDC) + if not args.dryrun: + os.makedirs(env_directory, exist_ok=True, mode=0o700) + if 1 in steps: + if not args.quiet: + print(termcol.BOLD+termcol.CYAN+"1) Creating \"base\" environment ..."+termcol.ENDC) + create_base_environment(cfg) + if 2 in steps: + if not args.quiet: + print(termcol.BOLD+termcol.CYAN+f"2) Creating \"condainer\" environment from {cfg['environment_yml']} ..."+termcol.ENDC) + create_condainer_environment(cfg) + if 3 in steps: + if not args.quiet: + print(termcol.BOLD+termcol.CYAN+f"3) Adding packages from {cfg['requirements_txt']} via pip ..."+termcol.ENDC) + pip_condainer_environment(cfg) + if 4 in steps: + if not args.quiet: + print(termcol.BOLD+termcol.CYAN+"4) Cleaning environments from unnecessary files ..."+termcol.ENDC) + clean_environment(cfg) + if 5 in steps: + if not args.quiet: + print(termcol.BOLD+termcol.CYAN+"5) Compressing installation directory into SquashFS image ..."+termcol.ENDC) + compress_environment(cfg) + if 6 in steps: + if not args.quiet: + print(termcol.BOLD+termcol.CYAN+"6) Creating activate and deactivate scripts ..."+termcol.ENDC) + if args.dryrun: + print("dryrun: skipping") + else: + write_activate_script(cfg) + write_deactivate_script(cfg) except: raise finally: - shutil.rmtree(env_directory) + if not args.quiet: + print(termcol.BOLD+termcol.CYAN+"7) Cleaning up ..."+termcol.ENDC) + if args.dryrun: + print("dryrun: skipping") + else: + shutil.rmtree(env_directory) + if not args.quiet: + print(termcol.BOLD+"Done!"+termcol.ENDC) def mount(args): @@ -322,9 +382,12 @@ def mount(args): os.makedirs(env_directory, exist_ok=True, mode=0o700) squashfs_image = get_image_filename(cfg) cmd = f"squashfuse {squashfs_image} {env_directory}".split() - proc = subprocess.Popen(cmd, shell=False) - proc.communicate() - assert(proc.returncode == 0) + if args.dryrun: + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False) + proc.communicate() + assert(proc.returncode == 0) if not args.quiet: activate = get_activate_cmd(cfg) print(termcol.BOLD+"Environment usage in the present shell"+termcol.ENDC) @@ -340,10 +403,13 @@ def umount(args): if is_mounted(cfg): env_directory = get_env_directory(cfg) cmd = f"fusermount -u {env_directory}".split() - proc = subprocess.Popen(cmd, shell=False) - proc.communicate() - assert(proc.returncode == 0) - shutil.rmtree(env_directory) + if args.dryrun: + print(f"dryrun: {' '.join(cmd)}") + else: + proc = subprocess.Popen(cmd, shell=False) + proc.communicate() + assert(proc.returncode == 0) + shutil.rmtree(env_directory) if not args.quiet: # print(termcol.BOLD+"OK"+termcol.ENDC) pass @@ -397,4 +463,5 @@ def test(args): """ # cfg = get_cfg() # print(is_mounted(cfg)) + print(args) pass diff --git a/condainer/main.py b/condainer/main.py index eb73c63..2fd8d75 100644 --- a/condainer/main.py +++ b/condainer/main.py @@ -17,11 +17,13 @@ def get_args(): ) parser.add_argument('-q', '--quiet', action='store_true', help='be quiet, do not write to stdout unless an error occurs') parser.add_argument('-d', '--directory', help='condainer project directory, the default is the current working directory') + parser.add_argument('-y', '--dryrun', action='store_true', help='dry run, do not actually do any operations, instead print information on what would be done') subparsers = parser.add_subparsers(dest='subcommand', required=True) subparsers.add_parser('init', help='initialize directory with config files') - subparsers.add_parser('build', help='build containerized conda environment') + parser_build = subparsers.add_parser('build', help='build containerized conda environment') + parser_build.add_argument('-s', '--steps', type=str, default="1,2,3,4,5,6,7", help='debug option to select individual build steps, default is all steps: 1,2,3,4,5,6,7') parser_exec = subparsers.add_parser('exec', help='execute command within containerized conda environment') parser_exec.add_argument('command', type=str, nargs='+', help='command line of the containerized command') -- GitLab